Overerving en polymorfisme · oefening
Respecteer gedragsverwachtingen
Een subklasse kan correct erven, netjes overschrijven en toch het programma stukmaken.
Elke functie die een concrete subklasse van Question ontvangt is geschreven met verwachtingen: is_correct ontvangt één antwoord en geeft een booleaanse waarde terug, points is een getal en describe geeft een string terug. De basismethode gooit alleen NotImplementedError op om een onvolledig familielid te markeren; het basisobject zelf mag niet in een quiz worden geplaatst. Elk concreet lid belooft het booleaanse gedrag. Python dwingt die belofte niet voor ons af.
Een subklasse die ongemerkt afwijkt is het onderwerp van deze les.
De belofte breken
Geen fout, en de score klopt toevallig: None is falsy, dus het lege antwoord leverde niets op. Kijk nu hoe dezelfde subklasse code tegenkomt die de vraag iets anders stelt:
Nul onjuiste antwoorden van een cursist die niets heeft beantwoord. None is False is False, dus het lege antwoord wordt als juist noch onjuist geteld en verdwijnt uit het rapport.
De subklasse deed op zichzelf iets redelijks. Die brak een belofte die de basis aan alle anderen had gedaan.
Vervangbaarheid
De geschonden regel heeft een naam die het kennen waard is: een subklasse moet overal bruikbaar zijn waar de basis wordt verwacht, zonder dat de omliggende code zich anders gedraagt. Als een functie met Question werkt en verkeerd gedrag vertoont wanneer die een TextQuestion krijgt, zit de fout in de subklasse, niet in de functie.
In de praktijk komt dit neer op een paar concrete beloften:
Geef hetzelfde soort waarde terug. Als de basis True of False teruggeeft, geef dan True of False terug. Een derde mogelijkheid toevoegen betekent dat elke ooit geschreven aanroeper een ongeteste route heeft.
Accepteer minstens wat de basis accepteert. Een subklasse waarvan is_correct een tweede argument vereist maakt elke bestaande aanroep stuk.
Voeg geen nieuwe vereisten toe. Als de basis altijd kan worden gebruikt, heeft een subklasse die een exceptie opgooit tenzij er eerst iets is ingesteld de afspraak beperkt.
Gooi niets op wat de basis niet zou opgooien. Aanroepers handelen af wat de basis documenteert. Een nieuw exceptietype gaat daar rechtstreeks langs.
Welke subklasse zou je veilig overal kunnen gebruiken waar een Question wordt verwacht?
Het lastige geval: een subklasse die minder kan
De klassieke versie van dit probleem is het bekijken waard, omdat die zo redelijk lijkt.
Stel dat Question ondersteunt dat de vraag anders wordt geformuleerd:
class Question:
def reword(self, prompt):
self.prompt = prompt
Een LockedQuestion, gebruikt in een eindtoets, mag nu nooit veranderen:
class LockedQuestion(Question):
def reword(self, prompt):
raise PermissionError("a locked question cannot be reworded")
Dit leest als zorgvuldig ontwerp, maar betekent dat code met een Question niet meer veilig reword kan aanroepen. Elke aanroeper heeft nu een controle of een try nodig en de belofte van de basis is waardeloos.
De fout lag eerder: LockedQuestion is geen soort Question zoals Question was gedefinieerd, omdat een vraag was gedefinieerd als iets dat anders geformuleerd kan worden. De opties gaan allemaal over ontwerp, niet over syntaxis:
haal
rewordvan de basis en zet die op een subklasseMutableQuestion;maak vragen bevroren waarden, zoals in hoofdstuk 8, zodat geen enkele anders kan worden geformuleerd;
geef
Questioneencan_reworddie de aanroeper kan opvragen, zodat weigeren deel van de afspraak is in plaats van een schending ervan.
Elk hiervan is beter dan een subklasse die faalt wanneer die als basis wordt gebruikt.
Een nuttige gewoonte
Als je een subklasse schrijft, kijk dan naar de code die de basis al gebruikt en vraag of die nog steeds correct zou zijn als die zonder het te weten jouw subklasse ontving.
Die vraag is makkelijker te beantwoorden met tests. Daarom komt hoofdstuk 13 hierop terug: één reeks tests schrijven voor de beloften van de basis en die uitvoeren voor elk familielid is de goedkoopste manier om deze problemen te vinden voordat een cursist dat doet.
De oefening geeft je vier subklassen. Drie breken een belofte en een deel van het werk is aanwijzen welke dat niet doet.
De diagnosefunctie heeft ook een klassenaam nodig: type(question).__name__ geeft die naam als string. In tegenstelling tot scorecode rapporteert deze functie expliciet welke klasse moet worden hersteld. Controleer een teruggegeven waarde met is True of is False: alleen gelijkheid accepteert ook 1 en 0. Hier is except Exception passend rond de diagnoseaanroep, omdat elke gewone exceptie de belofte schendt; registreer de klasse in plaats van de fout stilzwijgend te verbergen.
Opdracht
Elke concrete implementatie van Question.is_correct(response) belooft één ding: geef bij elke mogelijke string True of False terug. De tijdelijke basismethode gooit alleen een exceptie op omdat een kale Question onvolledig is en niet in de collectie mag komen. Drie van deze vier concrete subklassen breken de belofte.
Herstel elke subklasse zodat die de belofte nakomt en nog steeds doet wat die probeerde te doen:
UnansweredAwaregeeftNoneterug voor een leeg antwoord. Een leeg antwoord is simpelweg niet juist.PointsReturninggeeft de verdiende punten terug in plaats van een booleaanse waarde. Die moet een booleaanse waarde teruggeven en de aanroeperpointslaten lezen voor het aantal.StrictQuestiongooitValueErrorop voor een leeg antwoord. Die moet een leeg antwoord als onjuist behandelen.NumericQuestionis al correct. Laat die met rust.
Maak daarna check_family(questions) af, dat de namen teruggeeft van klassen die de belofte nog steeds breken. Geef elke vraag een leeg antwoord en een onzinantwoord en meld de klasse als het resultaat niet precies True of False is, of als het vragen een exceptie opgooit.
Bij een correct herstelde familie moet check_family een lege lijst teruggeven.