Gültigen Objektzustand schützen · Übung
Informationen mit Properties berechnen
attempt.current_score() schützt den Punktestand. Es macht die Klasse auch etwas umständlicher als die fehlerhafte Version, und das sollten wir offen betrachten:
print(attempt.score) # was: readable, and dangerously assignable
print(attempt.current_score()) # is: safe, and now a function call
Jedes Programm, das attempt.score verwendet hat, muss sich ändern. Alle, die künftig den Code lesen, müssen sich merken, welche Werte des Objekts einfache Attribute und welche Methodenaufrufe sind. Diesen echten Aufwand tragen alle, die die Klasse verwenden, um ein Problem in ihrem Inneren zu lösen.
Python bietet einen Weg, diesen Aufwand zu vermeiden.
Ein Attribut, das Code ausführt
Eine Property ist eine Methode, die aufrufender Code ohne Klammern erreicht, als wäre sie ein Attribut:
Wieder attempt.score, ohne Klammern. Die Zeile @property über der Methode macht das möglich. Python erkennt den Zugriff, führt die Methode aus und liefert ihren Rückgabewert.
Die Zeile mit @ heißt Decorator. Decorators als allgemeines Konzept lernst du später kennen. Betrachte @property vorerst als die Schreibweise für diese Funktion.
Was das gebracht hat
Die Schnittstelle ist wieder wie zuvor, und der Schutz bleibt:
attempt.score = 500
Eine Zuweisung an eine Property, die nur einen Getter definiert, löst AttributeError aus. Der Widerspruch vom Kapitelanfang ist über diesen Zugriff nun tatsächlich unmöglich, statt durch einen Unterstrich lediglich unerwünscht zu sein.
Noch interessanter: Die Klasse kann nun vollständig auf _score verzichten.
Es gibt jetzt überhaupt kein Attribut _score mehr. Der Punktestand wird bei jeder Abfrage aus den gespeicherten Ergebnissen berechnet, und jedes Programm, das attempt.score verwendet, funktioniert unverändert weiter. Das ist der Gewinn aus der Trennung von Schnittstelle und Repräsentation in der vorherigen Lektion.
Die zweite Version von Attempt speichert niemals einen Punktestand. Warum lässt sich attempt.score trotzdem lesen?
Ein gespeicherter Wert kann veralten
Bei Bedarf zu rechnen verhindert eine Art von Fehler, die leicht entsteht und schwer auffällt. Betrachte eine Version, die eine laufende Summe speichert und das Entfernen von Antworten erlaubt:
def undo_last(self):
self._results.pop() # the results changed
# and _score did not
Bei einem gespeicherten _score muss diese Methode daran denken, die Punkte abzuziehen. Sobald das jemand vergisst, meldet das Objekt einen Punktestand, der nicht zu seinen Ergebnissen passt. Mit einer berechneten Property ist undo_last in dieser Form vollständig: Der nächste Lesezugriff auf score spiegelt die tatsächlichen Ergebnisse wider, weil er daraus abgeleitet wird.
Der Preis dafür ist Rechenarbeit bei jedem Lesen. Bei einer Summe über eine Handvoll Ergebnisse fällt das nicht ins Gewicht. Wäre eine Berechnung aufwendig und würde ständig abgefragt, wäre Speichern vernünftig. Die Klasse wäre dann dafür verantwortlich, den gespeicherten Wert korrekt zu halten. Bevorzuge die Berechnung, bis du einen durch Messungen belegten Grund dagegen hast.
Wann eine Property nicht passt
Eine Property sollte sich anfühlen, als würdest du ein Objekt nach einer Information über sich selbst fragen. Das Lesen sollte schnell sein und niemanden überraschen.
Wenn die Arbeit langsam ist, auf ein Netzwerk zugreift oder beim Lesen etwas verändert, schreibe eine gewöhnliche Methode. attempt.score vermittelt eine Information über das Objekt. attempt.recalculate_from_server() vermittelt einen Vorgang. Einen Vorgang hinter Attributsyntax zu verstecken führt leicht zu einer Schleife, die unbemerkt tausend Anfragen sendet.
Aufgabe
Die bisher erarbeitete Klasse Attempt berechnet score und response_count bereits aus ihrer einzigen Liste _results. Ergänze die noch fehlende schreibgeschützte Property accuracy: den Anteil der Antworten, die Punkte eingebracht haben, als Zahl zwischen 0.0 und 1.0.
Ein Versuch ohne Antworten hat eine Genauigkeit von 0.0. Zu entscheiden, wie du in diesem Fall mit der Division umgehst, ist der Kern der Übung.
Führe das Programm aus und vergleiche die drei Angaben nach undo_last(). Die letzte Antwort war falsch: Nach ihrem Entfernen bleibt der Punktestand bei 2, die Anzahl sinkt von 2 auf 1, und die Genauigkeit steigt von 0.5 auf 1.0. Kein Code in undo_last muss diese Werte einzeln aktualisieren.