0%

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:

Try it

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:

Try it

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.

Zwei Darstellungen einer Ausnahmekette. Beim impliziten Kontext speichert das Auslösen von QuizError innerhalb einer ValueError-Behandlung den ValueError in __context__. Der Traceback sagt, dass während der Behandlung eine weitere Ausnahme auftrat. Bei einer ausdrücklichen Ursache speichert das Auslösen von QuizError mit from und dem abgefangenen Fehler den ValueError in __cause__ und kennzeichnet ihn als direkte Ursache.

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.

Try it

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:

DiagnoseinformationNutzen
Ein spezifischer TypDamit ein Aufrufer genau diesen Fall abfangen kann
Eine Meldung, die den Wert nenntDamit Lesende nicht weitersuchen müssen
Attribute mit den WertenDamit Code reagieren kann, ohne Text zu zerlegen
Eine Ursache bei einer ÜbersetzungDamit 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.