Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung Nächste Überarbeitung | Vorherige Überarbeitung | ||
talit:tutorial_oop3 [2025-04-26 20:28] – [Klassen und Objekte] hof | talit:tutorial_oop3 [2025-05-26 14:11] (aktuell) – [Aufgabe F] hof | ||
---|---|---|---|
Zeile 70: | Zeile 70: | ||
**Beispiel**: | **Beispiel**: | ||
- | Eine Klasse wird in Python mit dem `class` Keyword definiert. Methoden haben immer einen ersten | + | Eine Klasse wird in Python mit dem `class` Keyword definiert. Methoden haben immer einen ersten |
Am bestem am Beispiel: | Am bestem am Beispiel: | ||
Zeile 82: | Zeile 82: | ||
self.word2 = word2 # but not necessarily the same contents. | self.word2 = word2 # but not necessarily the same contents. | ||
| | ||
- | def reverse(self): | + | def reversed(self): |
- | """ | + | """ |
return WordPair(self.word2, | return WordPair(self.word2, | ||
- | tree = WordPair(' | + | tree = WordPair(' |
print(f' | print(f' | ||
- | |||
</ | </ | ||
+ | |||
+ | #### Terminologie | ||
+ | |||
+ | Alle Objekte mit den gleichen Eigenschaften werden durch eine Klasse (en. _class_) beschrieben. Die Eigenschaften sind [[wpde> | ||
+ | |||
+ | 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`. | ||
+ | #### Coding Conventions | ||
+ | * Klassennamen in `CamelCase` | ||
+ | * Methoden und Attribute in `lower_case_with_underscores()` | ||
+ | * Dokumentation mit `""" | ||
+ | * Der doc-string für die ganze Klasse beschreibt deren Funktion. Typischerweise ist der erste Satz die Vervollständigung des Satzanfangs "Ein < | ||
+ | * Der doc-string für Methoden beschreibt die Tätigkeit der Methode. Typischerweise beginnt der Satz mit einem Verb und ist die Vervollständigung des Satzanfangs "Die < | ||
+ | |||
### Aufgabe A | ### Aufgabe A | ||
Zeile 97: | Zeile 111: | ||
* Füge das `.gitignore` für Python hinzu (von [[https:// | * Füge das `.gitignore` für Python hinzu (von [[https:// | ||
- | Erstelle eine neue Python-Datei `voci.py` und erstelle deine erste Klasse `WordPair` wie oben und zeige sie der Lehrperson. Achte auf: | + | 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. |
- | * Coding-Conventions: | + | |
- | * Klassennamen in CamelCase | + | |
- | * Methoden | + | |
- | * Dokumentation | + | |
- | Wir möchten zusätzlich eine Klasse `VocabularyUnit` haben, die eine Liste von `WordPairs` speichert. | + | Wir möchten zusätzlich eine Klasse `VocabularyUnit` haben, die eine Liste von `WordPair`s speichert. |
- | Als drittes benötigen wir eine Klasse `ConsoleLearner` mit einer Methode `learn(unit)`, | + | Als drittes benötigen wir eine Klasse `ConsoleLearner` mit einer Methode `learn(unit)`, |
* um das Terminal zu leeren kannst du `print(" | * um das Terminal zu leeren kannst du `print(" | ||
- | * um die Zeile im Terminal zu löschen, verwendest du `print(" | + | * um nur die aktuelle |
* mehr dazu auf [[wp> | * mehr dazu auf [[wp> | ||
+ | |||
+ | ## Objekte und Klassen in Python | ||
+ | < | ||
+ | |||
+ | 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: | ||
+ | |||
+ | < | ||
+ | print(f' | ||
+ | print(f'" | ||
+ | |||
+ | Sogar Funktionen sind Objekte: | ||
+ | |||
+ | < | ||
+ | |||
+ | ### Vererbung | ||
+ | |||
+ | 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: | ||
+ | |||
+ | < | ||
+ | def __init__(self, | ||
+ | self.name = name # Attribute shared between all objects. | ||
+ | | ||
+ | def make_sound(self): | ||
+ | print(' | ||
+ | |||
+ | class Cat(Animal): | ||
+ | """ | ||
+ | def make_sound(self): | ||
+ | print(self.name + ': meow') | ||
+ | |||
+ | class Dog(Animal): | ||
+ | """ | ||
+ | def make_sound(self): | ||
+ | print(self.name + ': woof') | ||
+ | |||
+ | for animal in [Cat(" | ||
+ | animal.make_sound()</ | ||
+ | | ||
+ | Wenn nicht anders angegeben, erbt eine Klasse direkt von der `object` Klasse. Diese definiert [[https:// | ||
+ | |||
+ | < | ||
+ | pair = Animal() | ||
+ | print(pair)</ | ||
+ | |||
+ | Überschreiben wir die `__str__` Methode, können wir bestimmen, wie Objekte in Strings umgewandelt werden: | ||
+ | |||
+ | < | ||
+ | def __init__(self, | ||
+ | self.name = name # Attribute shared between all objects. | ||
+ | | ||
+ | def __str__(self): | ||
+ | return f'I am {self.name}, | ||
+ | |||
+ | class Cat(Animal): | ||
+ | """ | ||
+ | |||
+ | class Dog(Animal): | ||
+ | """ | ||
+ | |||
+ | for animal in [Animal(" | ||
+ | print(animal)</ | ||
+ | |||
+ | |||
+ | |||
+ | ### Aufgabe B: Statistik | ||
+ | |||
+ | Wir möchten aufzeichnen, | ||
+ | |||
+ | 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. | ||
+ | |||
+ | #### Composition | ||
+ | Wo sollen wir die Statistik in unserem Programm unterbringen? | ||
+ | #### Score berechnen | ||
+ | Der Score soll folgende Eigenschaften haben: | ||
+ | * der Score soll immer zwischen 0 und 1 sein. | ||
+ | * zu Beginn ist der Score 0. | ||
+ | * `0` bedeutet, dass das Wort unbekannt ist oder immer falsch war. | ||
+ | * `1` bedeutet, dass das Wort unendlich mal richtig getestet wurde. | ||
+ | * Der neueste Versuch soll mehr Gewicht haben als lange zurückliegende Versuche. | ||
+ | |||
+ | 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}$$ | ||
+ | |||
+ | |||
+ | #### Ausgabe | ||
+ | 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 [[https:// | ||
+ | |||
+ | < | ||
+ | 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 | ||
+ | </ | ||
+ | |||
+ | ## Refactoring | ||
+ | |||
+ | Dein Code in `ConsoleLearner.learn()` könnte irgendwie so aussehen: | ||
+ | |||
+ | <code python> | ||
+ | def learn(self, unit): | ||
+ | for pair in unit.pairs: | ||
+ | guess = input(f' | ||
+ | correct = guess == pair.word2 | ||
+ | pair.stats.record(correct) | ||
+ | if correct: | ||
+ | print(' | ||
+ | else: | ||
+ | print(f' | ||
+ | </ | ||
+ | |||
+ | Wir möchten einige Teile dieses Verhaltens anpassen: | ||
+ | ### Auswahl eines Wortes | ||
+ | Statt immer alle Paare durchzugehen, | ||
+ | * zufällige Wahl eines Wortpaars | ||
+ | * gewichtete Auswahl der Wortpaare: je schlechter der Score eines Paars, desto wahrscheinlicher dessen Wahl. | ||
+ | |||
+ | Statt unseren Code oben mit all diesen Varianten vollzukleistern, | ||
+ | |||
+ | <code python> | ||
+ | class LearningStrategy: | ||
+ | def select(self, | ||
+ | pass # has to be implemented by subclasses. | ||
+ | </ | ||
+ | |||
+ | Hier ein Beispiel für das lineare Durchgehen der Paare, wie im ursprünglichen Code: | ||
+ | |||
+ | <code python> | ||
+ | class LinearStrategy(LearningStrategy): | ||
+ | """ | ||
+ | def __init__(self): | ||
+ | self.index = 0 | ||
+ | |||
+ | def select(self, | ||
+ | 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: | ||
+ | |||
+ | <code python> | ||
+ | def learn(self, unit, learning_strategy=LinearStrategy()): | ||
+ | for _ in range(len(unit.pairs)): | ||
+ | pair = learning_strategy.select(unit) | ||
+ | guess = input(f' | ||
+ | correct = guess == pair.word2 | ||
+ | pair.stats.record(correct) | ||
+ | if correct: | ||
+ | print(' | ||
+ | else: | ||
+ | print(f' | ||
+ | </ | ||
+ | |||
+ | ### Aufgabe C: Bessere Auswahlstrategien | ||
+ | Schreibe zwei Auswahl-Strategien mit folgenden Eigenschaften, | ||
+ | |||
+ | * `RandomStrategy`: | ||
+ | * `ScoreStrategy`: | ||
+ | * s. [[https:// | ||
+ | |||
+ | Du hast damit das [[wp> | ||
+ | ### Aufgabe D: Wie lange lernen? | ||
+ | Ähnlich wie die Entscheidung über das nächste Wortpaar möchten wir auch die Entscheidung, | ||
+ | |||
+ | Schreibe eine Klasse `StopCriterion` | ||
+ | |||
+ | * `ScoreCriterion`: | ||
+ | * `TimeCriterion`: | ||
+ | * `CountingCriterion`: | ||
+ | * `OrCriterion`: | ||
+ | |||
+ | {{: | ||
+ | |||
+ | Wie sieht die `ConsoleLearner.learn` Methode jetzt aus? | ||
+ | ## Speichern & Lesen | ||
+ | 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, | ||
+ | |||
+ | <code python> | ||
+ | {" | ||
+ | </ | ||
+ | |||
+ | Eine ganze Unit wäre dann eine Liste solcher Objekte: | ||
+ | |||
+ | <code python> | ||
+ | [ | ||
+ | { | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | }, | ||
+ | { | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | ] | ||
+ | </ | ||
+ | ### Aufgabe E: JSON-Serialisierung | ||
+ | |||
+ | * Füge eine Methode `to_dict(self)` zu `WordPair` hinzu, die ein Wort-Paar und seine Stats in ein Dictionary verwandelt und zurückgibt. | ||
+ | * Füge eine Methode `save_to(self, | ||
+ | |||
+ | <code python> | ||
+ | def save_to(self, | ||
+ | json_list = [pair.to_dict() for pair in self.pairs] | ||
+ | import json | ||
+ | with open(filename, | ||
+ | json.dump(json_list, | ||
+ | </ | ||
+ | |||
+ | |||
+ | ### Statische Methoden | ||
+ | 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. | ||
+ | |||
+ | <code python> | ||
+ | class WordPair: | ||
+ | ... | ||
+ | | ||
+ | @staticmethod | ||
+ | def from_dict(data): | ||
+ | """ | ||
+ | pair = WordPair(data[' | ||
+ | # TODO also read stats if available | ||
+ | |||
+ | |||
+ | class VocabularyUnit: | ||
+ | ... | ||
+ | | ||
+ | @staticmethod | ||
+ | def read_from(filename): | ||
+ | """ | ||
+ | import json | ||
+ | pairs = [] | ||
+ | with open(filename, | ||
+ | json_pairs = json.load(infile) | ||
+ | pairs = [WordPair.from_dict(p) for p in json_pairs] | ||
+ | return VocabularyUnit(pairs) | ||
+ | </ | ||
+ | ### Aufgabe F | ||
+ | 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: | ||
+ | |||
+ | <code python> | ||
+ | from voci import * | ||
+ | |||
+ | filename = ' | ||
+ | unit = VocabularyUnit.read_from(filename) | ||
+ | |||
+ | learner = ConsoleLearner() | ||
+ | try: | ||
+ | learner.learn(unit) | ||
+ | unit.print_stats() | ||
+ | finally: | ||
+ | unit.save_to(filename) | ||
+ | </ | ||
+ | ### Aufgabe G - Webapp | ||
+ | S. auch [[talit: |