Fehler als Teil des Programmentwurfs · Übung
Einen Fehler aus einer tieferen Ebene übersetzen
Manche Fehler kommen aus einer tieferen Ebene mit einer Meldung, die für ein anderes Publikum geschrieben wurde.
parse_score("eight")
invalid literal for int() with base 10: 'eight'. Diese Meldung handelt von int(). Wer parse_score aufgerufen hat, dachte an einen Punktestand und muss jetzt herausfinden, was int() damit zu tun hat.
Unten abfangen, aussagekräftig auslösen
Die Lösung: Fange den Fehler der tieferen Ebene ab und löse einen aus, der die Sprache des Aufrufers spricht:
parse_score("eight")
Nun handelt die Meldung von Punkteständen, also vom Thema der Funktion. Das ist Übersetzung: Ein Fehler überschreitet die Grenze zwischen zwei Programmebenen und wird dabei neu formuliert.
Dieses Muster verdient einen Namen, weil es weit über int() hinausgeht. Eine Funktion, die eine Datei liest, löst „Datei nicht gefunden“ aus. Ihr Aufrufer möchte „Kein gespeichertes Quiz für diesen Lernenden“. Dasselbe Ereignis, andere Begriffe, und der Aufrufer darf die zweite Formulierung erwarten.
Die Ursache erhalten
Die obige Version lässt den Zusammenhang implizit. Du kannst ihn untersuchen:
__cause__ ist None, doch der ursprüngliche Fehler von int() ist weiterhin als impliziter __context__ vorhanden. Python zeigt normalerweise beide Ausnahmen im Traceback mit dem verbindenden Text „During handling…“. Dieser automatische Kontext ist nützlich. Er verrät aber nicht, ob der Zusammenhang eine bewusste Übersetzung oder einfach ein zweiter Fehler in der Fehlerbehandlung war.
raise ... from ... macht diesen Zusammenhang ausdrücklich:
Jetzt hält die Ausnahme das Original ausdrücklich in __cause__ fest: Deine Meldung ist für die Person, die sie liest; das Original darunter ist für die Person, die den Fehler beheben muss. In einem echten Traceback gibt Python beide aus, verbunden durch „The above exception was the direct cause of the following exception“. Nutze from, um eine bewusste Übersetzung eindeutig zu kennzeichnen, nicht etwa, weil der implizite Kontext sonst verschwinden würde.
Was fügt raise ValueError("...") from error gegenüber raise ValueError("...") hinzu?
An einer Grenze übersetzen, nicht überall
Eine Übersetzung kostet ein try und eine Fehlerbehandlung. Sie lohnt sich deshalb nur dort, wo sich die Begriffe tatsächlich ändern.
Eine Übersetzung lohnt sich. Ein Speicherfehler wird zu einem Domänenfehler. Ein Fehler beim Einlesen wird zu „Diese Quizdatei ist fehlerhaft“. Alles, bei dem die Meldung der tieferen Ebene ein Werkzeug nennt, von dem der Aufrufer noch nie gehört hat.
Eine Übersetzung lohnt sich nicht. Einen Fehler innerhalb derselben Schicht weiterzureichen, wobei sich nur der Wortlaut ändert. ValueError mit derselben Information erneut als ValueError auszulösen fügt einen Aufrufrahmen hinzu, aber keine Bedeutung.
Es gibt auch eine ausgesprochen schädliche Version:
try:
return int(text)
except ValueError:
return 0
Das ist keine Übersetzung, sondern Verschleierung. Ein fehlerhafter Punktestand wird zu einer plausibel wirkenden Null. Der Bericht sagt dann, dass der Lernende keine Punkte erzielt hat, statt dass seine Datei defekt ist. Wenn du nicht sagen kannst, was ein Wert bedeutet, erfinde keinen.
Mit bloßem raise erneut auslösen
Manchmal möchtest du auf einen Fehler reagieren und ihn trotzdem weitergeben. Ein bloßes raise innerhalb einer Fehlerbehandlung löst die abgefangene Ausnahme mit unverändertem Traceback erneut aus:
Aufzeichnen und behandeln sind unterschiedliche Entscheidungen. So kannst du das Erste tun, ohne vorzutäuschen, auch das Zweite erledigt zu haben.
Aufgabe
Drei Funktionen lesen Rohwerte und reichen sie an Code weiter, der in Quizbegriffen denkt. Jede soll in den Begriffen des Aufrufers scheitern, nicht denen des Werkzeugs.
parse_points(text) gibt die Punkte als Ganzzahl zurück. Bei nicht interpretierbarem Text löst die Funktion ValueError aus, nennt die Punkte und zeigt den problematischen Text. Das Original bleibt als Ursache erhalten.
lookup_question(bank, prompt) gibt die unter prompt gespeicherte Frage zurück. Fehlt der Fragetext, löst die Funktion ValueError aus und nennt den Fragetext, wieder mit dem ursprünglichen KeyError als Ursache. Eine fehlende Frage ist eine ungültige Anfrage, kein ungültiger Schlüssel. Der Aufrufer soll nicht wissen müssen, dass die Sammlung ein Dictionary ist.
parse_row(parts) erstellt aus einer Liste mit zwei Elementen wie ["prompt", "3"] ein Paar (prompt, points). Bei einer Zeile mit der falschen Anzahl an Teilen wird ein ValueError ausgelöst, der die Zeile beschreibt. Die Punkte werden mit parse_points eingelesen, und dessen Fehler wird direkt weitergegeben: Er spricht bereits die richtige Sprache. Ihn erneut einzupacken würde einen Aufrufrahmen hinzufügen, aber keine Bedeutung.
Schließlich hängt audited_points(text, log) den String "bad points: <text>" an das Protokoll an und lässt den Fehler dann unverändert weiterwandern. Verwende ein bloßes raise.