Fehler als Teil des Programmentwurfs · Übung
Nützliche Diagnoseinformationen bewahren
Die Meldung einer Ausnahme wird einmal geschrieben und unter Druck gelesen. In dieser Lektion sorgst du dafür, dass sie noch etwas Nützliches aussagt, wenn jemand sie zu Gesicht bekommt.
Werte mitgeben, nicht nur Worte
Eine Meldung ist ein String. Ein Aufrufer, der mit den Einzelheiten etwas tun möchte, muss sie wieder daraus herauslesen. Gib der Ausnahme stattdessen Attribute:
super().__init__(...) reicht die Meldung nach oben weiter, damit die Ausnahme weiterhin ausgegeben werden kann. Die beiden Attribute stehen dem Code zur Verfügung, der die Zahlen braucht. Eine Fehlerbehandlung kann jetzt sagen „Du hast nach 9 gefragt, versuche 0 bis 1“, ohne einen Satz zerlegen zu müssen.
Das lohnt sich genau dann, wenn ein Aufrufer die Werte benötigt. Wenn niemals etwas error.index lesen wird, reicht die Meldung allein.
Die Ursache nicht verlieren
Lektion 4 hat raise ... from ... eingeführt. Hier ist es besonders wichtig, denn der Unterschied zeigt sich erst, wenn etwas schiefgegangen ist:
Ohne from ist __cause__ gleich None. Python hält das Original aber weiterhin automatisch in __context__ fest und zeigt normalerweise beide Ausnahmen mit dem verbindenden Text „During handling…“. Mit from ist das Original auch die ausdrückliche __cause__, und der Traceback kennzeichnet es als direkte Ursache. Dieser ausdrückliche Zusammenhang zeigt Werkzeugen und Lesenden, dass die Übersetzung beabsichtigt war.
Impliziter Kontext kann durch eine bewusste Übersetzung ohne from entstehen oder durch einen unbeabsichtigten Fehler innerhalb einer Fehlerbehandlung. Eine ausdrückliche Ursache beseitigt diese Mehrdeutigkeit.

Warum ist raise QuizError(...) from error gegenüber raise QuizError(...) vorzuziehen?
Drei Wege, Informationen zu verlieren
Verschlucken. except QuizError: pass löscht das Ereignis. Nichts weiß mehr, dass es passiert ist.
Verflachen. except QuizError as error: raise QuizError("something went wrong") ersetzt die oberste Meldung durch eine vage. Das Original bleibt normalerweise als Kontext im Traceback, aber die Aufrufer erhalten jetzt eine weniger hilfreiche Ausnahme ohne die ursprünglichen Attribute.
Protokollieren statt behandeln. Einen Fehler aufzuzeichnen und dann so weiterzumachen, als wäre er nicht passiert, ist dasselbe wie Verschlucken, nur mit einer Protokollspur, die niemand liest. Wenn du protokollierst und nicht sicher fortfahren kannst, löse erneut aus.
Ein bloßes raise erhält die aktive Ausnahme und ihren Traceback unverändert. raise error löst dasselbe Objekt erneut aus, fügt aber die aktuelle Auslösestelle zum Traceback hinzu. raise QuizError(str(error)) erzeugt dagegen eine andere Ausnahme und verliert strukturierte Einzelheiten, sofern du sie nicht bewusst wiederherstellst.
Wie ein guter Fehlerfall aussieht
Zusammengenommen hat ein gut untersuchbarer Fehler vier Merkmale:
| Diagnoseinformation | Nutzen |
|---|---|
| Ein spezifischer Typ | Damit ein Aufrufer genau diesen Fall abfangen kann |
| Eine Meldung, die den Wert nennt | Damit Lesende nicht weitersuchen müssen |
| Attribute mit den Werten | Damit Code reagieren kann, ohne Text zu zerlegen |
| Eine Ursache bei einer Übersetzung | Damit das zugrunde liegende Ereignis erhalten bleibt |
Nicht jeder Fehler braucht alle vier. Ein ValueError für einen leeren Fragetext braucht die ersten beiden und nicht mehr. Entscheidend ist, dass jedes Merkmal eine bewusste Entscheidung ist. Die Voreinstellung „Irgendetwas auslösen und hoffen“ trifft alle vier schlecht.
In der Übung baust du eine Ausnahme, die ihren Kontext mitträgt, und eine Grenze, die ihn nutzt.
Aufgabe
Mache die Fehlerfälle gut untersuchbar.
QuestionNotFound(index, available) hält beide Werte als Attribute fest und reicht mit super().__init__ eine Meldung nach oben weiter, sodass bei der Ausgabe no question at 9; this bank has 2 erscheint.
InvalidResponse(prompt, response) tut dasselbe: Die Ausnahme hält prompt und response fest, mit einer Meldung, die beide nennt.
load_points(raw) übersetzt den Fehler von int() in QuizError und erhält das Original als Ursache.
describe(error) verwandelt eine abgefangene Ausnahme in "<TypeName>: <message>" und hängt " (caused by <CauseTypeName>)" an, wenn eine Ursache vorhanden ist. Die Funktion liest __cause__, nicht den Meldungstext.
collect(bank, answers, log) ist die Grenze. Bei jeder Ablehnung hängt die Funktion describe(error) an das Protokoll an und macht weiter; sie gibt den Punktestand zurück. Ein TypeError ist keine Ablehnung. Er wird deshalb protokolliert und dann mit einem bloßen raise erneut ausgelöst, wobei der ursprüngliche Traceback unverändert bleibt.