Fehler als Teil des Programmentwurfs · Übung
Einen Fehler durch zusammenarbeitende Objekte verfolgen
Eine ausgelöste Ausnahme bleibt nicht dort, wo sie entstanden ist. Sie wandert zurück durch alle Aufrufe, die auf eine Antwort warten, bis etwas sie abfängt oder das Programm endet.
Bei einem Programm aus einer einzigen Funktion ist das kaum eine Erwähnung wert. Bei einem Programm aus zusammenarbeitenden Objekten ist es die nützlichste Eigenschaft von Ausnahmen.
Den Weg beobachten
Übergib nun eine Zahl, wo Text erwartet wurde:
run_quiz(attempt, questions, [42])
Der TypeError wurde in Question.points_for ausgelöst. Weder Attempt.answer noch run_quiz erwähnt überhaupt Fehler, trotzdem wurden beide unterbrochen. Der Traceback führt sie alle auf, den innersten Aufruf zuletzt:
File "...", line 33, in run_quiz
attempt.answer(question, response)
File "...", line 27, in answer
self.score = self.score + question.points_for(response)
File "...", line 12, in points_for
raise TypeError(f"a response must be text, not {type(response).__name__}")
TypeError: a response must be text, not int
Lies einen Traceback von unten nach oben. Die letzte Zeile sagt, was schiefgegangen ist. Der Aufrufrahmen darüber sagt, wo. Die weiteren Rahmen zeigen, wie du dorthin gelangt bist.
points_for löst eine Ausnahme aus, und weder answer noch run_quiz enthält ein try. Was passiert mit Attempt.answer?
Warum das nützlich ist
In der Mitte des Programms steht nichts über Fehler. Genau darum geht es. Attempt.answer hat eine Aufgabe. Müsste die Methode zusätzlich entscheiden, was bei jedem denkbaren Fehler einer Frage zu tun ist, würde ihre eigentliche Arbeit unter der Behandlung von Problemen verschwinden, die sie nicht lösen kann.
Eine Methode, die ein Problem nicht beheben kann, sollte es nicht abfangen. Die Ausnahme weiterzugeben ist keine Bequemlichkeit. Es ist die richtige Behandlung und kostet keinen zusätzlichen Code.
Vergleiche das mit der Alternative, die Programme ohne Ausnahmen verwenden müssen:
def answer(self, question, response):
earned, error = question.points_for(response)
if error is not None:
return None, error
self.score = self.score + earned
return earned, None
Jede Funktion gibt einen Wert und einen Fehler zurück, jeder Aufrufer prüft darauf, und die eigentliche Arbeit macht nun ein Drittel des Codes aus. Die Version mit Ausnahmen sagt dasselbe, ohne etwas hinzuzufügen.
Was die Ausnahme mitbringt
Eine Ausnahme trägt ihren Typ, ihre Meldung und die ganze Kette der Aufrufrahmen mit sich, die sie durchlaufen hat. Dank dieser Kette lässt sich ein Fehler, der vier Objekte tief entstanden ist, noch ganz oben untersuchen. Deshalb ist das Verschlucken einer Ausnahme teuer: Wer sie abfängt und einfach weitermacht, wirft die einzige Aufzeichnung des Geschehens weg.
Das Problem des teilweise geänderten Zustands
Es gibt einen echten Preis, auf den Kapitel 5 dich schon vorbereitet hat. Wenn eine Ausnahme eine Methode mittendrin unterbricht, bleibt alles bereits Erledigte bestehen:
Eine gespeicherte Antwort ohne zugehörige Punkte. Erst wurde an die Liste angehängt, dann kam die Ausnahme. Das Objekt ist jetzt inkonsistent.
Die Lösung ist die Regel aus Kapitel 5, und genau deshalb war sie wichtig: Erledige zuerst alles, was scheitern kann, und ändere erst dann den Zustand. Die beiden Zeilen zu vertauschen macht diese Methode sicher.
Die nächste Lektion entscheidet, wer die Ausnahme abfängt und wo.
Aufgabe
Mache Attempt.answer sicher gegen eine Unterbrechung. Die Methode speichert derzeit die Antwort, bevor sie die Frage nach den Punkten fragt. Bei einem Fehler bleibt deshalb eine Antwort ohne Punkte zurück. Ändere die Reihenfolge so, dass alles, was scheitern kann, vor jeder Zustandsänderung stattfindet.
Füge weder Attempt noch Question ein try hinzu. Keines der beiden Objekte kann eine unbrauchbare Antwort beheben, also sollte keines etwas abfangen.
Die vorgegebene Hilfsfunktion trace_of(action) durchläuft error.__traceback__ und gibt die Namen der Funktionen zurück, durch die eine Ausnahme gewandert ist. Lies sie und führe sie aus, um den Traceback greifbar zu machen, aber bearbeite sie nicht. Die direkte Arbeit mit Tracebacks ist nicht deine Aufgabe in dieser Übung.