Fehler als Teil des Programmentwurfs · Übung
Eine Domänenausnahme erstellen
Bisher war jeder Fehler eine eingebaute Ausnahme. Eingebaute Ausnahmen sind die richtige Ausgangswahl, doch eine Sache können sie nicht leisten.
Das Problem mit einem geliehenen Typ
Ein Quizprogramm löst bei einer leeren Antwort ValueError aus und verwendet außerdem int(), das bei nicht interpretierbarem Text ebenfalls ValueError auslöst. Ein Aufrufer, der den ersten Fall behandeln möchte, den zweiten aber nicht, kann sie nicht auseinanderhalten:
try:
run_quiz(quiz, responses)
except ValueError:
... # a blank response? a malformed number? something else entirely?
Das geht nur durch Lesen der Meldung. Sich am Meldungstext zu orientieren ist aber eine Gewohnheit, die bei der ersten Verbesserung des Wortlauts scheitert.
Die Lösung ist ein eigener Typ:
check(" ")
class QuizError(Exception) ist die gesamte Definition. Außer einem Docstring braucht sie keinen Rumpf, denn entscheidend ist ihre Identität: Ein Aufrufer kann nun except QuizError schreiben und genau die Fehler abfangen, die dieses Programm bewusst signalisiert, und nichts anderes.
Eine kleine Familie
Ein Ausnahmetyp sagt „Dieses Programm hat abgelehnt“. Einige wenige sagen, welche Art von Ablehnung es war:
QuestionNotFound erbt von QuizError, deshalb fängt except QuizError diese Ausnahme ab. Das ist hier der Nutzen der Ersetzbarkeit aus Kapitel 9: Ein Aufrufer, dem die Unterscheidung wichtig ist, fängt den spezifischen Typ ab. Ein anderer fängt die Basisklasse ab und erhält beide.
Das ist eine kleine Familie. Zwei oder drei Typen unter einer Basisklasse reichen fast immer aus. Eine Hierarchie mit fünfzehn Ausnahmeklassen ist ein Entwurf, den niemand im Kopf behalten kann. Jede Klasse muss ihren Platz dadurch rechtfertigen, dass sie an einer anderen Stelle abgefangen wird als ihre Geschwister.
Warum erbt InvalidResponse von QuizError statt direkt von Exception?
Wann du keine eigene erfinden solltest
Eine eigene Ausnahme lohnt sich, wenn ein Aufrufer plausiblerweise diesen Fall und keinen anderen abfangen möchte. Das ist das ganze Kriterium.
Definiere keine eigene, wenn eine eingebaute Ausnahme den Sachverhalt schon genau ausdrückt. Ein falscher Typ ist ein TypeError, überall, in jedem jemals geschriebenen Python-Programm. InvalidTypeError bringt deinen Aufrufern ein privates Wort für eine allgemein bekannte Idee bei.
Definiere nicht eine pro Auslösestelle. EmptyPromptError, BlankAnswerError und NegativePointsError sind drei Namen für „Diese Frage ist ungültig“. Niemand wird sie je getrennt abfangen.
Eine nützliche Reihenfolge: Beginne mit der eingebauten Ausnahme und führe erst dann einen eigenen Typ ein, wenn du sie zum ersten Mal unabhängig von einem anderen Fall abfangen musst.
Wo sie hingehören
Ein Ausnahmetyp gehört zur öffentlichen Schnittstelle eines Moduls: Aufrufer müssen ihn benennen können, um ihn abzufangen. Platziere diese Typen dort, wo Aufrufer mühelos auf sie zugreifen können. Für die Struktur aus Kapitel 10 heißt das: errors.py auf der obersten Ebene des Pakets, importiert von den Domänenmodulen, die sie auslösen.
Deshalb enthält auch die vorgeschriebene Struktur des Abschlussprojekts eine errors.py. Das ist keine Ablagekonvention, sondern der Ort, an dem ein Aufrufer nachsieht, welche Anfragen dieses Programm ablehnen kann.
Aufgabe
Gib dem Quizprogramm eine kleine Familie von Ausnahmen. Verwende überall dort eingebaute Ausnahmen, wo diese den Sachverhalt schon ausdrücken.
Definiere QuizError(Exception) als Basisklasse, darunter InvalidResponse und QuestionNotFound. Drei Klassen, die jeweils nur einen Docstring benötigen.
Verwende sie dann in QuestionBank:
question_at(index)löstQuestionNotFoundaus, wenn der Index außerhalb der Sammlung liegt, undTypeError, sofern der Typ des Index nicht exaktintist.TrueundFalsesind keine Positionen. Ein falscher Typ ist in jedem Python-Programm einTypeError, deshalb bleibt es dabei.respond(index, response)löst bei einer leeren AntwortInvalidResponseaus. Andernfalls vergleicht die Methode die Antwort, nachdem sie umgebenden Leerraum entfernt und sie in Kleinbuchstaben umgewandelt hat. Gib bei Übereinstimmung die Punkte zurück, bei einer falschen Antwort0.
Schließlich gibt run(bank, answers) ein Paar (score, refused) zurück. Dabei ist refused eine Liste von Ausnahmemeldungen. Die Funktion fängt pro Antwort einmal QuizError ab und zeichnet jede Meldung mit str(error) auf. Genau dafür gibt es die Basisklasse. Sie darf TypeError nicht abfangen: Ein falscher Typ ist ein Fehler im aufrufenden Code, keine Ablehnung durch dieses Programm.