Erstellen Sie eine Python-Multiplikationstabellen-App mit OOP

Eine Multiplikations-App mit Python und Objektorientierung entwickeln

In diesem Beitrag werden wir gemeinsam eine Anwendung für das Erlernen von Multiplikationstabellen entwickeln, indem wir uns die Stärken der objektorientierten Programmierung (OOP) in Python zunutze machen.

Dabei werden wir die zentralen Konzepte der OOP praktisch anwenden und demonstrieren, wie diese in einer voll funktionsfähigen App eingesetzt werden können.

Python ist eine vielseitige Programmiersprache, die uns als Entwickler die Flexibilität gibt, für jede Aufgabe die beste Vorgehensweise zu wählen. Die objektorientierte Programmierung ist hierbei ein sehr beliebtes Paradigma, das seit Jahren bei der Entwicklung großer Anwendungen eingesetzt wird.

Grundlagen der OOP

Werfen wir einen kurzen Blick auf das Herzstück der OOP in Python: die Klassen.

Eine Klasse ist wie ein Bauplan, der die Struktur und das Verhalten von Objekten definiert. Dieser Bauplan erlaubt es uns, Instanzen zu erzeugen, die nichts anderes als individuelle Objekte sind, die gemäß der Definition der Klasse erstellt wurden.

Eine einfache Klasse für ein Buch, mit den Eigenschaften Titel und Farbe, könnte folgendermaßen definiert sein:

class Buch:
    def __init__(self, titel, farbe):
        self.titel = titel
        self.farbe = farbe

Um Instanzen der Buch-Klasse zu erstellen, rufen wir die Klasse auf und übergeben ihr die entsprechenden Parameter.

# Instanzen der Klasse Buch
blaues_buch = Buch("Der blaue Junge", "Blau")
gruenes_buch = Buch("Die Froschgeschichte", "Grün")

Eine Visualisierung unseres aktuellen Programms könnte so aussehen:

Eine Besonderheit ist, dass die Überprüfung des Datentyps der Instanzen blaues_buch und gruenes_buch den Typ „Buch“ ergibt.

# Ausgabe des Typs der Bücher

print(type(blaues_buch))
# <class '__main__.Buch'>
print(type(gruenes_buch))
# <class '__main__.Buch'>

Nachdem wir diese Grundlagen verstanden haben, können wir unser Projekt angehen 😃.

Projektbeschreibung

Als Entwickler verbringen wir die meiste Zeit nicht mit dem Schreiben von Code. Laut einer Studie von thenewstack verwenden wir nur etwa ein Drittel unserer Zeit für das eigentliche Programmieren oder die Überarbeitung von Code.

Die übrigen zwei Drittel werden für das Lesen von fremdem Code und das Analysieren der vorliegenden Probleme aufgewendet.

Für dieses Projekt werde ich daher eine Aufgabenstellung entwerfen und wir werden gemeinsam analysieren, wie daraus unsere Anwendung entsteht. So durchlaufen wir den gesamten Prozess vom ersten Gedanken zur Lösung bis zur Umsetzung in Code.

Ein Lehrer an einer Grundschule möchte ein Spiel entwickeln, um das Multiplikationsvermögen von Schülern zwischen 8 und 10 Jahren zu überprüfen.

Das Spiel sollte ein Lebens- und Punktesystem beinhalten. Der Schüler startet mit drei Leben und muss eine bestimmte Punktzahl erreichen, um zu gewinnen. Wenn der Schüler alle seine Leben verliert, sollte eine Meldung „Verloren“ ausgegeben werden.

Das Spiel sollte zwei Modi haben: zufällige Multiplikationen und tabellarische Multiplikationen.

Im ersten Modus bekommt der Schüler zufällige Multiplikationsaufgaben mit Zahlen von 1 bis 10 gestellt. Bei richtiger Antwort gibt es einen Punkt. Bei falscher Antwort verliert der Schüler ein Leben und das Spiel geht weiter. Gewonnen hat der Schüler, sobald er 5 Punkte erreicht hat.

Der zweite Modus stellt eine Multiplikationstabelle von 1 bis 10 dar. Der Schüler muss das Ergebnis jeder Aufgabe eingeben. Bei drei Fehlern verliert er das Spiel, bei zwei vollständig gelösten Tabellen ist das Spiel gewonnen.

Ich weiß, die Anforderungen sind umfangreich, aber ich verspreche, dass wir sie in diesem Beitrag gemeinsam meistern werden 😁.

Teile und Herrsche

Die wichtigste Fähigkeit in der Programmierung ist das Lösen von Problemen. Es erfordert einen Plan, bevor man mit dem Schreiben von Code beginnt.

Ich empfehle immer, ein komplexes Problem in kleinere, leichter zu lösende Teile zu zerlegen.

Wenn Sie ein Spiel entwickeln, ist es am besten, zunächst die wesentlichen Komponenten zu identifizieren. Diese Teilprobleme sind viel einfacher anzugehen.

Erst dann wissen Sie, wie Sie alles mit Code umsetzen und integrieren können.

Erstellen wir also ein Diagramm, wie das Spiel aufgebaut sein wird.

Dieses Diagramm veranschaulicht die Beziehungen zwischen den Objekten unserer Anwendung. Wie wir sehen, sind die beiden zentralen Objekte die zufällige Multiplikation und die tabellarische Multiplikation. Beide haben die Attribute Punkte und Leben gemeinsam.

Mit all diesen Informationen können wir nun mit dem Codieren beginnen.

Entwicklung der übergeordneten Spielklasse

In der objektorientierten Programmierung ist es wichtig, Code-Wiederholungen zu vermeiden. Dies wird als DRY-Prinzip (Don’t Repeat Yourself) bezeichnet.

Hinweis: Es geht nicht darum, weniger Codezeilen zu schreiben (die Codequalität sollte nicht danach bewertet werden), sondern um die Abstraktion häufig verwendeter Logik.

Gemäß diesem Prinzip sollte die übergeordnete Klasse unserer Anwendung die Struktur und das gewünschte Verhalten der beiden anderen Klassen definieren.

Hier ist die Umsetzung:

class BasisSpiel:

    # Länge für die Zentrierung der Nachricht
    nachrichten_laenge = 60

    beschreibung = ""

    def __init__(self, punkte_zum_gewinnen, anzahl_leben=3):
        """Basisklasse für das Spiel

        Args:
            punkte_zum_gewinnen (int): Die Anzahl der Punkte, die zum Gewinnen benötigt werden.
            anzahl_leben (int): Die Anzahl der Leben, die der Spieler hat. Standardwert ist 3.
        """
        self.punkte_zum_gewinnen = punkte_zum_gewinnen

        self.punkte = 0

        self.leben = anzahl_leben

    def get_numerische_eingabe(self, nachricht=""):

        while True:
            # Benutzereingabe abfragen
            benutzer_eingabe = input(nachricht)

            # Wenn die Eingabe eine Zahl ist, wird sie zurückgegeben
            # Andernfalls wird eine Meldung ausgegeben und die Eingabe wiederholt
            if benutzer_eingabe.isnumeric():
                return int(benutzer_eingabe)
            else:
                print("Die Eingabe muss eine Zahl sein")
                continue

    def print_willkommensnachricht(self):
        print("PYTHON MULTIPLIKATIONSSPIEL".center(self.nachrichten_laenge))

    def print_verloren_nachricht(self):
        print("LEIDER HABEN SIE ALLE LEBEN VERLOREN".center(self.nachrichten_laenge))

    def print_gewonnen_nachricht(self):
        print(f"HERZLICHEN GLÜCKWUNSCH, SIE HABEN {self.punkte} PUNKTE ERREICHT".center(self.nachrichten_laenge))

    def print_aktuelle_leben(self):
        print(f"Aktuell haben Sie {self.leben} Leben\n")

    def print_aktuelle_punkte(self):
        print(f"\nIhr Punktestand ist {self.punkte}")

    def print_beschreibung(self):
        print("\n\n" + self.beschreibung.center(self.nachrichten_laenge) + "\n")

    # Grundlegende Ausführungsmethode
    def ausfuehren(self):
        self.print_willkommensnachricht()

        self.print_beschreibung()

Das ist eine umfangreiche Klasse. Hier eine detaillierte Erklärung:

Beginnen wir mit den Klassenattributen und dem Konstruktor.

Klassenattribute sind Variablen, die innerhalb der Klasse, aber außerhalb des Konstruktors oder einer Methode erstellt werden.

Instanzattribute hingegen sind Variablen, die nur innerhalb des Konstruktors erstellt werden.

Der Hauptunterschied liegt im Geltungsbereich. Klassenattribute sind sowohl von einem Instanzobjekt als auch von der Klasse zugänglich. Instanzattribute können nur von einem Instanzobjekt aufgerufen werden.

spiel = BasisSpiel(5)

# Zugriff auf das Klassenattribut nachrichten_laenge von der Instanz aus
print(spiel.nachrichten_laenge) # 60

# Zugriff auf das Klassenattribut nachrichten_laenge von der Klasse aus
print(BasisSpiel.nachrichten_laenge)  # 60

# Zugriff auf das Instanzattribut punkte von der Instanz aus
print(spiel.punkte) # 0

# Zugriff auf das Instanzattribut punkte von der Klasse aus
print(BasisSpiel.punkte) # Attributfehler

Ein weiterer Artikel könnte dieses Thema vertiefen. Bleiben Sie auf dem Laufenden.

Die Funktion get_numerische_eingabe verhindert, dass der Benutzer nicht-numerische Eingaben macht. Diese Methode fragt den Benutzer so lange, bis er eine numerische Eingabe macht. Wir werden sie später in den Kindklassen verwenden.

Die Ausgabemethoden vermeiden, dass wir die gleichen Ausgaben wiederholt schreiben müssen, wenn ein Ereignis im Spiel eintritt.

Die Methode ausfuehren ist lediglich ein Wrapper, den die Klassen Zufällige Multiplikation und Tabellenmultiplikation verwenden, um mit dem Benutzer zu interagieren und alles zum Laufen zu bringen.

Entwicklung der Kindklassen

Nachdem wir die übergeordnete Klasse erstellt haben, die die Struktur und einige Funktionen unserer Anwendung definiert, ist es nun an der Zeit, die eigentlichen Spielmodusklassen mithilfe der Vererbung zu entwickeln.

Klasse für zufällige Multiplikationen

Diese Klasse implementiert den „ersten Modus“ unseres Spiels. Dabei verwenden wir das Zufallsmodul, das es uns ermöglicht, den Benutzer nach zufälligen Operationen von 1 bis 10 zu fragen. Hier ist ein informativer Beitrag über das Zufallsmodul (und andere wichtige Module) 😉.

import random # Modul für zufällige Operationen
class ZufallsMultiplikation(BasisSpiel):

    beschreibung = "In diesem Spiel müssen Sie die zufälligen Multiplikationsaufgaben richtig beantworten.\nSie gewinnen, wenn Sie 5 Punkte erreichen oder verlieren, wenn Sie alle Leben verlieren."

    def __init__(self):
        # Für das Gewinnen sind 5 Punkte erforderlich
        # Der Wert 5 wird als "punkte_zum_gewinnen"-Parameter übergeben
        super().__init__(5)

    def get_zufallszahlen(self):

        erste_zahl = random.randint(1, 10)
        zweite_zahl = random.randint(1, 10)

        return erste_zahl, zweite_zahl

    def ausfuehren(self):

        # Aufruf der Oberklasse zum Ausgeben der Willkommensnachrichten
        super().ausfuehren()

        while self.leben > 0 and self.punkte_zum_gewinnen > self.punkte:
            # Zwei Zufallszahlen werden abgerufen
            zahl1, zahl2 = self.get_zufallszahlen()

            operation = f"{zahl1} x {zahl2}: "

            # Der Benutzer wird aufgefordert, die Operation zu beantworten
            # Zur Vermeidung von Fehlerwerten
            benutzer_antwort = self.get_numerische_eingabe(message=operation)

            if benutzer_antwort == zahl1 * zahl2:
                print("\nIhre Antwort ist richtig\n")

                # Punkt wird hinzugefügt
                self.punkte += 1
            else:
                print("\nLeider ist Ihre Antwort falsch\n")

                # Leben wird abgezogen
                self.leben -= 1

            self.print_aktuelle_punkte()
            self.print_aktuelle_leben()

        # Wird nur ausgeführt, wenn das Spiel beendet ist
        # Und keine der Bedingungen mehr erfüllt ist
        else:
            # Ausgabe der Abschlussnachricht

            if self.punkte >= self.punkte_zum_gewinnen:
                self.print_gewonnen_nachricht()
            else:
                self.print_verloren_nachricht()

Dies ist eine weitere umfangreiche Klasse 😅. Aber wie bereits erwähnt, kommt es nicht auf die Anzahl der Zeilen an, sondern auf die Lesbarkeit und Effizienz. Und das Schöne an Python ist, dass es Entwicklern ermöglicht, übersichtlichen und lesbaren Code zu schreiben, fast so, als würden sie normales Englisch sprechen.

Es gibt einen Aspekt in dieser Klasse, der möglicherweise Fragen aufwirft, aber ich werde versuchen, ihn so einfach wie möglich zu erklären.

    # Übergeordnete Klasse
    def __init__(self, punkte_zum_gewinnen, anzahl_leben=3):
        "...
    # Untergeordnete Klasse
    def __init__(self):
        # Für das Gewinnen sind 5 Punkte erforderlich
        # Der Wert 5 wird als "punkte_zum_gewinnen"-Parameter übergeben
        super().__init__(5)

Der Konstruktor der Kindklasse ruft die super-Funktion auf, die sich gleichzeitig auf die übergeordnete Klasse (BasisSpiel) bezieht. Im Wesentlichen wird Python angewiesen:

Fülle das Attribut „punkte_zum_gewinnen“ der übergeordneten Klasse mit dem Wert 5!

Es ist nicht nötig, self in den super().__init__() Teil zu schreiben, da der super-Aufruf im Konstruktor selbst zu Redundanz führen würde.

Wir verwenden die super-Funktion auch in der Methode ausfuehren und werden sehen, was in diesem Codeabschnitt geschieht.

    # Grundlegende Ausführungsmethode
    # Übergeordnete Methode
    def ausfuehren(self):
        self.print_willkommensnachricht()

        self.print_beschreibung()
    def ausfuehren(self):

        # Aufruf der Oberklasse zum Ausgeben der Willkommensnachrichten
        super().ausfuehren()

        .....

Wie in der run-Methode der übergeordneten Klasse zu sehen, werden die Willkommens- und Beschreibungsnachrichten ausgegeben. Es ist jedoch empfehlenswert, diese Funktionalität beizubehalten und in den Kindklassen weitere zu ergänzen. Daher nutzen wir super, um den gesamten Code der übergeordneten Methode auszuführen, bevor der nächste Teil ausgeführt wird.

Der restliche Teil der ausfuehren-Funktion ist relativ einfach. Der Benutzer wird nach einer Zahl mit der Aufgabe gefragt, die er beantworten soll. Dann wird das Ergebnis mit der korrekten Antwort verglichen. Bei Übereinstimmung wird ein Punkt hinzugefügt, ansonsten wird ein Leben abgezogen.

Es ist erwähnenswert, dass wir while-else-Schleifen verwenden. Dies würde den Rahmen dieses Artikels sprengen, aber ich werde dazu in Kürze einen Artikel veröffentlichen.

Die Methode get_zufallszahlen verwendet die Funktion random.randint, die eine zufällige Ganzzahl innerhalb des angegebenen Bereichs zurückgibt. Sie gibt dann ein Tupel aus zwei zufälligen ganzen Zahlen zurück.

Klasse für tabellarische Multiplikationen

Der „zweite Modus“ des Spiels muss das Einmaleins tabellarisch anzeigen und sicherstellen, dass der Benutzer mindestens 2 Tabellen korrekt beantwortet.

Dazu werden wir wieder die Stärken von super nutzen und das übergeordnete Klassenattribut punkte_zum_gewinnen auf 2 ändern.

class TabellenMultiplikation(BasisSpiel):

    beschreibung = "In diesem Spiel müssen Sie die gesamte Multiplikationstabelle korrekt lösen.\nSie gewinnen, wenn Sie 2 Tabellen abschließen."

    def __init__(self):
        # Für das Gewinnen sind 2 Tabellen erforderlich
        super().__init__(2)

    def ausfuehren(self):

        # Ausgabe der Willkommensnachrichten
        super().ausfuehren()

        while self.leben > 0 and self.punkte_zum_gewinnen > self.punkte:
            # Zufallszahl zwischen 1 und 10 holen
            zahl = random.randint(1, 10)

            for i in range(1, 11):

                if self.leben <= 0:
                    # Verhindern, dass das Spiel fortgesetzt wird,
                    # wenn der Benutzer keine Leben mehr hat

                    self.punkte = 0
                    break

                operation = f"{zahl} x {i}: "

                benutzer_antwort = self.get_numerische_eingabe(message=operation)

                if benutzer_antwort == zahl * i:
                    print("Super! Ihre Antwort ist richtig")
                else:
                    print("Leider ist Ihre Antwort falsch")

                    self.leben -= 1

            self.punkte += 1

        # Wird nur ausgeführt, wenn das Spiel beendet ist
        # Und keine der Bedingungen mehr erfüllt ist
        else:
            # Ausgabe der Abschlussnachricht

            if self.punkte >= self.punkte_zum_gewinnen:
                self.print_gewonnen_nachricht()
            else:
                self.print_verloren_nachricht()

Wie Sie sehen können, ändern wir nur die Methode ausfuehren dieser Klasse. Das ist die Magie der Vererbung. Wir schreiben die Logik, die wir verwenden, einmalig und verwenden sie dann an mehreren Stellen 😅.

In der Methode ausfuehren verwenden wir eine for-Schleife, um die Zahlen von 1 bis 10 zu durchlaufen und die Operation zu erstellen, die dem Benutzer angezeigt wird.

Wenn die Leben erschöpft sind oder die zum Gewinnen erforderlichen Punkte erreicht sind, wird die while-Schleife beendet und die Gewinn- oder Verlustmeldung ausgegeben.

WUNDERBAR, wir haben die zwei Spielmodi erstellt, aber bisher passiert nichts, wenn wir das Programm ausführen.

Vervollständigen wir das Programm mit der Implementierung der Modusauswahl und instanziieren die Klassen basierend auf der Auswahl.

Implementierung der Auswahl

Der Benutzer kann nun auswählen, welchen Modus er spielen möchte. Hier die Implementierung:

if __name__ == "__main__":

    print("Spielmodus auswählen")

    wahl = input("[1],[2]: ")

    if wahl == "1":
        spiel = ZufallsMultiplikation()
    elif wahl == "2":
        spiel = TabellenMultiplikation()
    else:
        print("Bitte wählen Sie einen gültigen Spielmodus")
        exit()

    spiel.ausfuehren()

Zuerst wird der Benutzer aufgefordert, zwischen den Modi 1 oder 2 zu wählen. Wenn die Eingabe nicht gültig ist, wird das Skript nicht weiter ausgeführt. Wenn der Benutzer den ersten Modus wählt, führt das Programm den zufälligen Multiplikationsspielmodus aus. Bei der Wahl des zweiten Modus wird der Tabellenmultiplikationsmodus ausgeführt.

So würde es aussehen.

Zusammenfassung

Herzlichen Glückwunsch, Sie haben gerade mit objektorientierter Programmierung eine Python-Anwendung erstellt.

Der gesamte Code ist im Github-Repository verfügbar.

In diesem Beitrag haben Sie gelernt:

  • Python-Klassenkonstruktoren zu verwenden
  • Eine funktionierende Anwendung mit OOP zu entwickeln
  • Die super-Funktion in Python-Klassen zu verwenden
  • Die Grundlagen der Vererbung anzuwenden
  • Klassen- und Instanzattribute zu implementieren

Viel Spaß beim Programmieren 👨‍💻

Erforschen Sie als Nächstes einige der besten Python-IDEs für eine verbesserte Produktivität.