Code mit vielen Eigenschaften kann schnell unübersichtlich werden - sogar selbst geschriebener Code altert schlecht und ist nach wenigen Wochen unlesbar. Was macht der folgende Code?
v = [ [["Baum", "tree"], 0, 0], [["Blume", "flower"], 0, 0], [["Fisch", "fish"], 0, 0], ] for _ in range(3): for w in v: w1, w2 = w[0] if w2 == input(f"Translate {w1}: "): w[1]+=1 print("correct") else: w[2]+=1 print("incorrect") for w in v: print(f'{w[1]/(w[1]+w[2]):.0%}: {w[0]}')
Code wird nur einmal geschrieben, aber sehr viel häufiger und von mehr Personen gelesen. Es lohnt sich also, in leserlichen Code zu investieren. Natürlich könnten wir obigen Spaghetti-Code mit besseren Variablennamen, Dictionaries und Kommentaren verbessern. Nur verlieren wir vor lauter Strings schnell den Überblick…
# Define vocabulary unit voci_unit = [ {'translation': {'word1':"Baum", 'word2': "tree"}, 'correct': 0, 'incorrect': 0}, {'translation': {'word1':"Blume", 'word2': "flower"}, 'correct': 0, 'incorrect': 0}, {'translation': {'word1':"Fisch", 'word2': "fish"}, 'correct': 0, 'incorrect': 0}, ] # Make three passes for _ in range(3): for w in voci_unit: if w['translation']['word2'] == input(f"Translate {w['translation']['word1']}: "): w['correct']+=1 print("correct") else: w['incorrect']+=1 print("incorrect") # Print statistics for w in voci_unit: print(f'{w['correct']/(w['correct']+w['incorrect']):.0%}: {w['translation']}')
Wouldn't it be nice… wenn wir unsere Lernprogramm ohne viel Kommentar und trotzdem kurz und verständlich definieren könnten?
Zum Beispiel so:
from voci import * unit = VocabularyUnit([ WordPair('Baum', 'tree'), WordPair('Blume', 'flower'), WordPair('Fisch', 'fish') ]) learner = ConsoleLearner() learner.learn(unit) unit.print_stats()
Genau das erreichen wir mit der objekt-orientierten Programmierung. Du hast bereits sehr oft Objekte benutzt, zum Beispiel Strings oder Listen. Ein Objekt gruppiert Attribute (en. attributes) und Methoden (Funktionen, die zum Objekt gehören). Alle Objekte einer Klasse haben die gleichen Methoden und die gleichen Attribut-Namen, aber möglicherweise unterschiedliche Inhalte in den Attributen.
Beispiel: Alle Listen-Objekte enthalten eine Sequenz von Elementen, aber nicht dieselben. Die pop
-Methode wird aber für alle Listen-Objekte das letzte Element entfernen und zurückgeben.
Eine Klasse wird in Python mit dem class
Keyword definiert. Methoden haben immer einen ersten Parameter self
, der für das konkrete Objekt steht, für das die Methode ausgeführt wird. Wird eine neues Objekt erstellt, wird zuerst die __init__
Methode aufgerufen.
Am bestem am Beispiel:
class WordPair: """A word and its translation.""" def __init__(self, word1, word2): """Creates a new pair from two strings.""" self.word1 = word1 # All objects of class WordPair share the same attributes self.word2 = word2 # but not necessarily the same contents. def reversed(self): # All objects of class WordPair share the same methods """Returns a copy of the pair in the reverse direction.""" return WordPair(self.word2, self.word1) tree = WordPair('Baum', 'tree') # Calls __init__, we are passing only two arguments, the self argument is implicit. print(f'{tree.word1} translates to {tree.word2}') # Attributes can be accessed using dot-notation.
Alle Objekte mit den gleichen Eigenschaften werden durch eine Klasse (en. class) beschrieben. Die Eigenschaften sind Attribute (Objekt-Variablen) und Methoden (Objekt-Funktionen).
Man kann sich ein Objekt vorstellen wie ein Dictionary mit definierten Keys, wobei die gespeicherten Werte entweder andere Objekte oder Funktionen sind. Wenn bekannt ist, dass eine Variable eine Objekt einer bestimmten Klasse ist, kann man sich darauf verlassen, dass die von der Klasse definierten Einträge vorhanden sind.
Objekte werden manchmal auch als Instanzen (einer Klasse) bezeichnet; im obigen Code-Beispiel ist das in der Variable tree
gespeicherte Objekt eine Instanz der Klasse WordPair
.
CamelCase
lower_case_with_underscores()
"""doc strings"""
ksr_talit_vocabulary
und clone es lokal in VS Code.tkilla77
und anschae
..gitignore
für Python hinzu (von hier).
Erstelle eine neue Python-Datei voci.py
und erstelle deine erste Klasse WordPair
wie oben und zeige sie der Lehrperson. Achte auf Coding Conventions und Dokumentation.
Wir möchten zusätzlich eine Klasse VocabularyUnit
haben, die eine Liste von WordPair
s speichert. Wie sieht die __init__
Methode dieser Klasse aus?
Als drittes benötigen wir eine Klasse ConsoleLearner
mit einer Methode learn(unit)
, die alle Wort-Paare abfragt.
print("\033c", end="")
benützenprint("\033[H\033[J", end="")
Jeder Wert in Python ist eigentlich ein Objekt. Zahlen sind Objekte der Klassen int
oder float
. Die Klasse eines Objekts kann mit der Funktion type()
herausgefunden werden:
Sogar Funktionen sind Objekte:
Klassen bilden eine Hierarchie: Eine Klasse kann von einer anderen Klasse erben; dier Basisklasse vererbt alle Eigenschaften (Attribute und Methoden) an ihre Unterklassen. Methoden können in den Unterklassen auch überschrieben werden. Jedes Objekt einer Unterklasse gehört automatisch auch zur Basisklasse.
Wir können die Vererbung explizit angeben:
Wenn nicht anders angegeben, erbt eine Klasse direkt von der object
Klasse. Diese definiert allerhand praktische Methoden, die wir aber überschreiben können. Beispielsweise definiert die __str__
Methode, wie ein Objekt in einen String umgewandelt wird. Dies wird von print
benützt und gibt im Allgemeinen die Klasse und die Speicheradresse des Objekts aus:
Überschreiben wir die __str__
Methode, können wir bestimmen, wie Objekte in Strings umgewandelt werden:
Wir möchten aufzeichnen, wie oft unsere Wörter richtig bzw. falsch übersetzt werden. Dafür benötigen wir eine Stats
-Klasse. Neben den Attributen correct
bzw. incorrect
möchten wir ein Attribut score
haben, das einen Wert zwischen [0..1]
bereitstellt, wobei 0
bedeutet, dass das Wort noch komplett unbekannt ist, und 1
, dass das Wort perfekt gelernt wurde.
Die Klasse Stats
soll eine Methode record(correct)
erhalten: Jedes Mal, wenn ein Wortpaar getestet wird, findet der Learner heraus, ob das Wort richtig oder falsch ist. Dieses Verdikt wird mit record()
an die Statistik weitergeleitet und dort aufgezeichnet.
Zudem sollte Stats
auch noch eine sinnvolle __str__
Methode haben.
Wo sollen wir die Statistik in unserem Programm unterbringen? Eigentlich soll jedes WordPair
über ein eigenes Stats
-Objekt verfügen. Gleichzeitig macht es keinen Sinn, ein Stats
ohne dazugehöriges WordPair
zu haben. Am besten fügen wir der Klasse WordPair
ein Attribut stats
hinzu. Wir sagen diesem Muster auch Composition
: ein WordPair
besteht aus zwei Strings und einem Stats-Objekt.
Der Score soll folgende Eigenschaften haben:
0
bedeutet, dass das Wort unbekannt ist oder immer falsch war.1
bedeutet, dass das Wort unendlich mal richtig getestet wurde.Es bietet sich an, mit einem Decay zu arbeiten: jedes Mal, wenn ein neuer Wert dazukommt, wird der alte Score mit einem Faktor <1 multipliziert. Mit einem Faktor von 0.5 setzt sich der Score zur Hälfte aus dem neuesten Test, zur anderen Hälfte aus dem bisherigen Score zusammen: $$\begin{aligned} score_{new} &= 0.5 \cdot (test_0 + score_{old}) \\ &= 0.5 \cdot (test_0 + 0.5 \cdot (test_1 + 0.5 \cdot (test_2 + \ldots))) \\ &= \frac{test_0}{2} + \frac{test_1}{4} + \frac{test_2}{8} + \frac{test_3}{16} + \ldots \end{aligned}$$
Nach einem Learning Run möchten wir alle WordPairs
mit ihren Statistiken ausgeben. Füge eine Methode print_stats()
zu VocabularyUnit
hinzu und verwende darin die __str__
Funktion von WordPair
.
Die Ausgabe soll z.B. wie folgt aussehen - und nach Score sortiert sein:
84% (22/25) Fluss -> river 98% (26/74) Haus -> house 100% (17/19) See -> lake 100% (28/38) Fisch -> fish 100% (23/27) Maus -> mouse 100% (24/35) Katze -> cat 100% (21/37) Tisch -> table 100% (23/36) Stuhl -> chair 100% (26/30) Baum -> tree 100% (32/46) Blume -> flower
Dein Code in ConsoleLearner.learn()
könnte irgendwie so aussehen:
def learn(self, unit): for pair in unit.pairs: guess = input(f'Translate {pair.word1}') correct = guess == pair.word2 pair.stats.record(correct) if correct: print('Yeah') else: print(f'Incorrect, {pair.word1} translates to {pair.word2}')
Wir möchten einige Teile dieses Verhaltens anpassen:
Statt immer alle Paare durchzugehen, möchten wir andere Auswahl-Strategien ermöglichen:
Statt unseren Code oben mit all diesen Varianten vollzukleistern, lagern wir die Auswahl in ein eigenes Objekt aus:
class LearningStrategy: def select(self, unit): pass # has to be implemented by subclasses.
Hier ein Beispiel für das lineare Durchgehen der Paare, wie im ursprünglichen Code:
class LinearStrategy(LearningStrategy): """A learning strategy that selects all word pairs in a unit, in order.""" def __init__(self): self.index = 0 def select(self, unit): pair = unit.pairs[self.index % len(unit.pairs)] self.index += 1 return pair
Wir passen ConsoleLearner.learn()
so an, dass eine Strategie mitgeliefert werden kann, aber ein sinnvoller Default-Wert ausgewählt wird:
def learn(self, unit, learning_strategy=LinearStrategy()): for _ in range(len(unit.pairs)): pair = learning_strategy.select(unit) guess = input(f'Translate {pair.word1}') correct = guess == pair.word2 pair.stats.record(correct) if correct: print('Yeah') else: print(f'Incorrect, {pair.word1} translates to {pair.word2}')
Schreibe zwei Auswahl-Strategien mit folgenden Eigenschaften, und teste sie aus.
RandomStrategy
: zufällige Wahl eines WortpaarsScoreStrategy
: gewichtete Auswahl der Wortpaare: je schlechter der Score eines Paars, desto wahrscheinlicher dessen Wahl.Du hast damit das Strategie-Entwurfsmuster kennengelernt.
Ähnlich wie die Entscheidung über das nächste Wortpaar möchten wir auch die Entscheidung, wie lange gelernt werden soll, abstrahieren.
Schreibe eine Klasse StopCriterion
mit einer Methode should_stop(self, unit)
. Implementiere verschiedenen Kriterien als Unterklassen von StopCriterion
, zum Beispiel:
ScoreCriterion
: stoppt, wenn der Score des schlechtesten WortPaars über 90% (oder einem konfigurierbaren Wert) liegt.TimeCriterion
: stoppt, wenn eine definierte Zeit verstrichen ist.CountingCriterion
: stoppt nach einer definierten Anzahl Wortpaare.OrCriterion
: kombiniert mehrere Kriterien, stoppt, wenn mindestens eines der Kriterien stoppt.
Wie sieht die ConsoleLearner.learn
Methode jetzt aus?
Wir möchten VocabularyUnits
in eine Datei speichern und von dort wieder lesen können. Es bietet sich an, eine Unit als JSON-Objekt zu speichern. JSON (JavaScript Object Notation) sind Dictionaries, die als Keys nur Strings, als Werte ausschliesslich Strings, Zahlen, Boolean-Werte, Listen und wiederum JSON-Objekte enthalten. Ein WordPair
könnte zum Beispiel so aussehen:
{"word1": "Baum", "word2": "tree", "correct": 26, "incorrect": 4, "score": 0.9999961480498314}
Eine ganze Unit wäre dann eine Liste solcher Objekte:
[ { "word1": "Baum", "word2": "tree", "correct": 24, "incorrect": 4, "score": 0.9249845921993256 }, { "word1": "Blume", "word2": "flower", "correct": 30, "incorrect": 14, "score": 0.9999847523718017 } ]
to_dict(self)
zu WordPair
hinzu, die ein Wort-Paar und seine Stats in ein Dictionary verwandelt und zurückgibt.save_to(self, filename)
zu VocabularyUnit
hinzu, die die ganze Unit in die angegebene Datei speichert, in dem alle Wort-Paar-Dictionaries in eine Liste eingefügt werden. Diese kann dann mit json.dump in eine Datei geschrieben werden:def save_to(self, filename): json_list = [pair.to_dict() for pair in self.pairs] import json with open(filename, 'w') as out: json.dump(json_list, out, indent=2) # indent=2 aligns the output nicely
Fürs Einlesen kommt die umgekehrte json.load
Funktion zum Einsatz. Allerdings haben wir noch ein kleines Problem: Eine VocabularyUnit existiert ja noch gar nicht, wenn wir sie einlesen wollen aus der Datei. Wir benötigen also eine Funktion, die nicht an eine bestimmte Unit gebunden ist. Diese werden mit @staticmethod
annotiert und haben keinen self
Parameter. Statische Funktionen werden direkt über den Klassennamen aufgerufen.
class WordPair: ... @staticmethod def from_dict(data): """Creates a fresh word pair from json data.""" pair = WordPair(data['word1'], data['word2']) # TODO also read stats if available class VocabularyUnit: ... @staticmethod def read_from(filename): """Reads a vocabulary unit from a file.""" import json pairs = [] with open(filename, 'r') as infile: json_pairs = json.load(infile) pairs = [WordPair.from_dict(p) for p in json_pairs] return VocabularyUnit(pairs)
Füge statische Methoden zu VocabularyUnit
und WordPair
hinzu, um die gespeicherten Daten wieder einlesen zu können.
Ein Beispielprogramm für unseren Code könnte nun so aussehen:
from voci import * filename = 'data/test.voci' unit = VocabularyUnit.read_from(filename) learner = ConsoleLearner() try: learner.learn(unit) unit.print_stats() finally: # Code im finally-Block wird jedenfalls ausgeführt, auch wenn eine Exception passiert ist. unit.save_to(filename)
S. auch Flask Webserver und Web Apps.