0%

Gültigen Objektzustand schützen · Übung

Beim Erzeugen validieren

Mit problems(question) stehen die Regeln an einer Stelle, doch eine Lücke bleibt: Irgendetwas muss daran denken, die Funktion aufzurufen. Eine ungeprüfte Frage ist genauso kaputt wie zuvor, und das Programm läuft weiter, bis der Schaden an einer ganz anderen Stelle sichtbar wird.

Es gibt einen Zeitpunkt, an dem jeder gewöhnliche Aufruf zur Objekterzeugung denselben Code durchläuft: __init__ wird ausgeführt, bevor der Aufruf das neue Objekt an den aufrufenden Code zurückgibt. Damit ist dies der naheliegende Ort, um ein ungültiges Objekt abzulehnen.

Ablehnen statt zurückgeben

In Grundlagen I hast du für ungültige Daten mit raise einen Fehler ausgelöst:

if points <= 0:
    raise ValueError("points must be greater than zero")

Dieselbe Anweisung funktioniert in __init__:

Try it

Die gültige Frage wird wie gewohnt erzeugt. Versuche jetzt eine ungültige:

Question("", "3", 2)

Python löst ValueError aus, der Konstruktoraufruf gibt kein Question-Objekt zurück, und die Zuweisung findet nicht statt. Der aufrufende Code erhält entweder ein gültiges Objekt oder eine Ausnahme.

Die Validierung im Konstruktor weist ungültige Eingaben zurück, bevor der aufrufende Code ein Question-Objekt erhält.

Erst prüfen, dann zuweisen

Diese Reihenfolge ist bewusst gewählt und verdient eine eigene Regel: Validiere, bevor du speicherst.

class Question:
    def __init__(self, prompt, answer, points):
        self.prompt = prompt        # stored
        if not prompt:              # checked afterwards
            raise ValueError("a question needs a prompt")

Diese Version löst denselben Fehler aus, ein schneller Test könnte sie also für richtig halten. Dennoch speichert sie Zustand, bevor sie entscheidet, ob er gültig ist. In einer größeren Initialisierung könnte eine später aufgerufene Hilfsfunktion oder ein beteiligtes Objekt diesen teilweise aufgebauten Zustand schon vor dem Fehler sehen. Wenn du zuerst alle vorgeschlagenen Werte prüfst, erhält die Klasse eine verlässliche Regel: Gespeicherter Zustand ist gültiger Zustand.

Was hinterlässt Question("", "3", 2), wenn __init__ einen ValueError auslöst?

Ein Fehler, mit dem der aufrufende Code etwas anfangen kann

ValueError passt hier, weil der übergebene Wert den richtigen Typ hat, aber inhaltlich unbrauchbar ist. Grundlagen I hat diese Ausnahme genau für diesen Fall eingeführt. Innerhalb einer Klasse gilt dieselbe Begründung.

Die Meldung ist genauso wichtig wie der Ausnahmetyp. Vergleiche:

raise ValueError("invalid")
raise ValueError("points must be greater than zero")

Die erste Meldung sagt der Person, die den Traceback liest, dass etwas falsch war. Die zweite sagt ihr, was sie ändern muss. Du schreibst diese Meldung für jemanden, der diesen Code nicht sehen kann, vielleicht für dich selbst in sechs Monaten.

Die Meldung auslesen

In Grundlagen I hast du Ausnahmen anhand ihres Typs abgefangen. Ein Ausnahmehandler kann der abgefangenen Ausnahme auch einen Namen geben und sie anschließend auslesen:

Try it

as error bindet das Ausnahmeobjekt innerhalb des Handlers an einen Namen. Wenn du es ausgibst, erscheint die Meldung, die du beim Auslösen angegeben hast. So prüfst du, ob deine Meldung beim Lesen hilft.

Mehr tut as vorerst nicht. Kapitel 12 greift auf, welche weiteren Informationen eine Ausnahme enthält.

Fehler sind nicht immer die Antwort

Ein wichtiger Hinweis, bevor du deine eigene Prüfung schreibst: In __init__ eine Ausnahme auszulösen ist richtig, wenn das Objekt so niemals nützlich sein könnte. Das trifft auf eine Frage ohne erwartete Antwort zu.

Es passt nicht für jeden unerwünschten Wert. Liest ein Programm Fragen aus einer Datei, bei der bekanntermaßen einige Zeilen unvollständig sind, ist es womöglich falsch, beim ersten ungültigen Datensatz das ganze Programm anzuhalten. Das Programm könnte diese Zeilen lieber überspringen und am Ende ihre Anzahl melden. In diesem Fall fängt der aufrufende Code den Fehler ab und entscheidet. Darum geht es in Kapitel 12.

Ein kaputtes Objekt gar nicht erst zu erzeugen bleibt der richtige Standard. Dadurch tritt der Fehler dort auf, wo die falsche Eingabe erfolgt, statt drei Funktionen später.

Aufgabe

Vervollständige __init__, sodass ein Question-Objekt nur erzeugt wird, wenn alle drei Regeln gelten: ein nicht leerer Fragetext, eine nicht leere erwartete Antwort und Punkte größer als null.

Löse ValueError mit einer Meldung aus, die die verletzte Regel benennt. Prüfe jede Regel, bevor du irgendein Attribut zuweist.

Das Programm am Ende erzeugt eine gültige Frage und versucht dann drei ungültige. Es gibt jeweils die Fehlermeldung aus. Führe es aus und lies, was jeder Fehler meldet.