Kapitel 12 · Übung
Fehler als Teil des Programmentwurfs
Eine passende eingebaute Ausnahme auslösen
Kapitel 5 hat mit raise ValueError eine Frage ohne Antwort abgelehnt und ist dann zum nächsten Thema übergegangen. Dieses Kapitel nimmt Fehler ernst: In einem Programm aus zusammenarbeitenden Objekten sind sie keine Zufälle. Sie gehören zur Schnittstelle.
Jede Methode, die du schreibst, gibt zwei Zusagen: was sie zurückgibt, wenn alles klappt, und was sie tut, wenn es nicht klappt. Die zweite blieb bisher unausgesprochen.
Ablehnen heißt nicht versagen
Eine Methode hat drei ehrliche Antwortmöglichkeiten, wenn sie die Anfrage nicht erfüllen kann:
Eine falsche Antwort ist kein Fehler. Sie ist ein gewöhnliches Ergebnis mit einem gewöhnlichen Wert, und 0 zurückzugeben ist richtig.
Vergleiche das mit der Anfrage nach einer Frage, die nicht existiert. Keine Zahl bedeutet „Diese Frage gab es nicht“. Wer eine erfindet, hat bald ein Programm, das Lernenden einen Punktestand von -1 meldet.
Die drei Antworten:
| Situation | Antwort |
|---|---|
| Ein gewöhnliches Ergebnis, auch ein unerwünschtes | Einen Wert zurückgeben |
| Das Fehlen ist zu erwarten und der Aufrufer prüft darauf | None zurückgeben |
| Der Aufrufer verlangt etwas Unmögliches | Eine Ausnahme auslösen |
Entscheidend ist, ob der Aufrufer vernünftigerweise damit rechnen konnte. Eine leere QuestionBank kann ein normaler Zwischenstand sein, während Fragen zusammengestellt werden. Ob ein ausführbares Quiz leer sein darf, ist eine separate Zusage für die Konstruktion, die das Kapitelprojekt ausdrücklich festlegt. Ein Quizindex von 99 in einer Sammlung mit zwei Fragen ist ein Fehler im aufrufenden Code. Ihn zu verbergen hilft niemandem.
Sobald raise ausgeführt wird, endet die Funktion sofort. Die Ausnahme breitet sich aus: Sie wird an den Aufrufer weitergegeben, bis eine passende Grenze sie behandelt. Deshalb gehört die Reihenfolge der Validierung zur Korrektheit: Führe Prüfungen, die scheitern können, vor dem Anhängen oder Ändern von Zustand aus. Eine abgelehnte Operation lässt das Objekt dann unverändert.
Den Typ wählen
Pythons eingebaute Ausnahmen sind keine Dekoration. Jede teilt dem Aufrufer etwas anderes mit. Den richtigen Typ zu wählen macht den Großteil der Arbeit aus:
| Ausnahme | Wann |
|---|---|
ValueError | Die richtige Art von Objekt, aber ein unbrauchbarer Wert. Ein leerer Fragetext, eine negative Punktzahl. |
TypeError | Eine völlig falsche Art von Objekt. Eine Liste, obwohl eine Frage erwartet wurde. |
KeyError | Ein fehlender Schlüssel in etwas, das über Schlüssel angesprochen wird. |
IndexError | Eine Position außerhalb einer Sequenz. |
LookupError | Einer der letzten beiden Fälle, wenn du dich nicht auf einen davon festlegen möchtest. |
quiz.question_at(9)
Der Aufrufer kennt jetzt den Unterschied zwischen „Du hast etwas vom falschen Typ übergeben“ und „Du hast nach einer nicht vorhandenen Position gefragt“ und kann beide Fälle unterschiedlich behandeln. Die Prüfung auf den exakten Typ ist Absicht: True und False verhalten sich bei manchen Python-Operationen wie die Ganzzahlen 1 und 0. Sie sind aber Entscheidungen und keine Fragepositionen, deshalb lehnt diese API sie ab.
quiz.question_at("first") wird mit einem String aufgerufen. Welche Ausnahme passt?
Meldungen, die die Suche beenden
Eine Meldung wird von jemandem gelesen, der deinen Code nicht sieht, in einem Traceback, vielleicht unter Zeitdruck. Sie sollte beantworten: Was wurde erwartet, was kam an und wo?
raise IndexError("bad index")
raise IndexError(f"no question at {index}; this quiz has {len(self._questions)}")
Die zweite Meldung beendet die Untersuchung. Die erste beginnt sie.
Nenne den problematischen Wert. Das kostet einen einzigen f-String und ist die nützlichste Information, die eine Meldung enthalten kann.
Was du nicht auslösen solltest
Exception selbst. raise Exception("something went wrong") gibt dem Aufrufer nichts, was er gezielt abfangen kann. Ihm bleibt nur, alles oder nichts abzufangen.
assert zur Prüfung von Eingaben. Zusicherungen sind für Dinge gedacht, die deiner Ansicht nach nicht passieren können. Außerdem lässt sich Python so ausführen, dass sie entfernt werden. Eine Regel für die Eingabe des Aufrufers ist keine Annahme, sondern eine Zusage. Zusagen brauchen raise.
In der Übung erhält eine Klasse drei Methoden, die derzeit Unsinn akzeptieren.
Aufgabe
QuestionBank nimmt derzeit Unsinn an und gibt Unsinn zurück. Gib jeder Methode die passende Ablehnung.
question_at(index) löst TypeError aus, wenn der Typ des Index nicht exakt int ist, also auch bei True und False, und IndexError, wenn er außerhalb der Sammlung liegt. Die TypeError-Meldung nennt den problematischen Typ, etwa str oder bool; die IndexError-Meldung nennt den problematischen Index.
add(prompt, answer, points) löst ValueError bei einem leeren Fragetext oder einem Fragetext aus ausschließlich Leerraum aus, ebenso bei einer Punktzahl unter null. Im letzteren Fall nennt die Meldung die negative Punktzahl. Validiere vor dem Anhängen, damit eine Ablehnung die Sammlung nicht verändert. Null Punkte sind hier erlaubt: Eine unbewertete Umfragefrage ist sinnvoll.
points_for(index, response) gibt bei einer richtigen Antwort die Punkte und bei einer falschen Antwort 0 zurück. Eine falsche Antwort ist ein gewöhnliches Ergebnis, kein Fehler. Dafür wird also keine Ausnahme ausgelöst.
find(prompt) gibt die passende Frage zurück oder None, wenn es keine gibt. Das Fehlen ist hier zu erwarten, und der Aufrufer prüft darauf.
Drei verschiedene Antworten auf „Ich kann dir das Angefragte nicht geben“. Zwischen ihnen zu wählen ist die Übung.