Vererbung und Polymorphie · Übung
Verhaltenszusagen einhalten
Eine Unterklasse kann korrekt erben und sauber überschreiben und das Programm trotzdem beschädigen.
Jede Funktion, die eine konkrete Unterklasse von Question entgegennimmt, wurde mit Erwartungen geschrieben: is_correct nimmt eine Antwort entgegen und gibt einen booleschen Wert zurück, points ist eine Zahl, und describe gibt einen String zurück. Die Basismethode löst NotImplementedError nur aus, um ein unvollständiges Familienmitglied zu kennzeichnen. Das Basisobjekt selbst darf nicht in ein Quiz gelangen. Jedes konkrete Mitglied verspricht das boolesche Verhalten. Python setzt diese Zusage nicht für uns durch.
Eine Unterklasse, die stillschweigend davon abweicht, ist das Thema dieser Lektion.
Die Zusage verletzen
Kein Fehler, und der Punktestand stimmt zufällig: None gilt als falsch, daher brachte die leere Antwort keine Punkte. Sieh nun, wie dieselbe Unterklasse auf Code trifft, der etwas anders fragt:
Null falsche Antworten, obwohl die Person nichts beantwortet hat. None is False ergibt False. Die leere Antwort wird daher weder als richtig noch als falsch gezählt und verschwindet aus dem Bericht.
Für sich genommen tat die Unterklasse etwas Vernünftiges. Sie verletzte aber eine Zusage, die die Basis allen anderen gegeben hatte.
Ersetzbarkeit
Die verletzte Regel hat einen Namen, den du kennen solltest: Eine Unterklasse soll überall verwendbar sein, wo ihre Basis erwartet wird, ohne dass sich der umgebende Code anders verhält. Funktioniert eine Funktion mit Question, aber nicht mit einem übergebenen TextQuestion, liegt der Fehler in der Unterklasse, nicht in der Funktion.
Praktisch bedeutet das einige konkrete Zusagen:
Gib dieselbe Art von Ergebnis zurück. Gibt die Basis True oder False zurück, gib True oder False zurück. Eine dritte Möglichkeit schafft in jedem vorhandenen Aufrufer einen ungeprüften Pfad.
Akzeptiere mindestens das, was die Basis akzeptiert. Eine Unterklasse, deren is_correct ein zweites Argument verlangt, beschädigt jeden bestehenden Aufruf.
Füge keine neuen Voraussetzungen hinzu. Ist die Basis jederzeit verwendbar, schränkt eine Unterklasse, die ohne vorherige Konfiguration eine Ausnahme auslöst, den Vertrag ein.
Löse keine Ausnahmen aus, die die Basis nicht auslösen würde. Aufrufender Code behandelt, was die Basis dokumentiert. Ein neuer Ausnahmetyp läuft an dieser Behandlung vorbei.
Welche Unterklasse wäre überall sicher verwendbar, wo ein Question erwartet wird?
Der schwierige Fall: eine Unterklasse, die weniger kann
Die klassische Form dieses Problems lohnt einen Blick, weil sie so vernünftig aussieht.
Angenommen, Question erlaubt, seinen Fragetext neu zu formulieren:
class Question:
def reword(self, prompt):
self.prompt = prompt
Ein LockedQuestion aus einer Abschlussprüfung darf sich dagegen niemals ändern:
class LockedQuestion(Question):
def reword(self, prompt):
raise PermissionError("a locked question cannot be reworded")
Das liest sich wie sorgfältiger Entwurf. Es bedeutet aber, dass Code mit einem Question nun nicht mehr sicher reword aufrufen kann. Jeder Aufrufer braucht eine Prüfung oder ein try, und die Zusage der Basis ist wertlos.
Der Fehler lag früher: LockedQuestion ist keine Art von Question, so wie Question hier definiert wurde, denn eine Frage wurde als umformulierbar definiert. Die Optionen betreffen den Entwurf, nicht die Syntax:
Entferne
rewordaus der Basis und lege es in eine UnterklasseMutableQuestion.Mache Fragen zu eingefrorenen Werten wie in Kapitel 8, sodass sich keine umformulieren lässt.
Gib
Questionein abfragbarescan_reword, sodass die Ablehnung Teil des Vertrags ist, statt ihn zu verletzen.
Jede dieser Möglichkeiten ist besser als eine Unterklasse, die bei Verwendung als ihre Basis scheitert.
Eine nützliche Gewohnheit
Wenn du eine Unterklasse schreibst, betrachte den Code, der bereits die Basis verwendet. Frage, ob er weiterhin korrekt wäre, wenn er deine Unterklasse erhielte, ohne davon zu wissen.
Mit Tests lässt sich diese Frage leichter beantworten. Deshalb greift Kapitel 13 sie wieder auf: Eine Testsammlung gegen die Zusagen der Basis zu schreiben und auf jedes Familienmitglied anzuwenden ist der günstigste Weg, solche Fehler vor den Lernenden zu entdecken.
Die Übung gibt dir vier Unterklassen. Drei verletzen eine Zusage. Ein Teil der Aufgabe ist zu erkennen, welche es nicht tut.
Ihre Diagnosefunktion braucht außerdem einen Klassennamen: type(question).__name__ liefert ihn als String. Anders als die Punktewertung meldet diese Funktion ausdrücklich, welche Klasse repariert werden muss. Prüfe einen Rückgabewert mit is True oder is False: Gleichheit allein akzeptiert auch 1 und 0. Hier ist except Exception um den Diagnoseaufruf passend, weil jede gewöhnliche Ausnahme die Zusage verletzt. Erfasse die Klasse, statt den Fehler still zu verbergen.
Aufgabe
Jede konkrete Implementierung von Question.is_correct(response) verspricht eines: Für einen beliebigen String gibt sie True oder False zurück. Der Basisplatzhalter löst nur deshalb eine Ausnahme aus, weil ein einfaches Question unvollständig ist und nicht in die Sammlung gelangen darf. Drei dieser vier konkreten Unterklassen verletzen die Zusage.
Repariere jede so, dass sie die Zusage einhält und trotzdem ihre beabsichtigte Aufgabe erfüllt:
UnansweredAwaregibt bei einer leeren AntwortNonezurück. Eine leere Antwort ist einfach nicht richtig.PointsReturninggibt verdiente Punkte statt eines booleschen Werts zurück. Es soll einen booleschen Wert zurückgeben und den aufrufenden Code die Zahl auspointslesen lassen.StrictQuestionlöst bei einer leeren AntwortValueErroraus. Es soll eine leere Antwort als falsch behandeln.NumericQuestionist bereits korrekt. Lass es unverändert.
Vervollständige dann check_family(questions), das die Namen aller Klassen zurückgibt, die die Zusage noch verletzen. Gib jeder Frage eine leere und eine unsinnige Antwort. Melde die Klasse, wenn das Ergebnis nicht exakt True oder False ist oder der Aufruf eine Ausnahme auslöst.
Bei einer korrekt reparierten Familie soll check_family eine leere Liste zurückgeben.