Fouten als onderdeel van programmaontwerp · oefening
Maak een domeinexceptie
Elke fout tot nu toe was ingebouwd. Ingebouwde excepties zijn de juiste standaardkeuze en er is één ding dat ze niet kunnen doen.
Het probleem met een geleend type
Een quizprogramma gooit ValueError op voor een leeg antwoord en gebruikt ook int(), dat ValueError opgooit voor tekst die niet kan worden geparseerd. Een aanroeper die de eerste wel en de tweede niet wil afhandelen, kan ze niet onderscheiden:
try:
run_quiz(quiz, responses)
except ValueError:
... # a blank response? a malformed number? something else entirely?
De enige manier om het te weten is het bericht lezen. Vergelijken op berichttekst is een gewoonte die stukgaat zodra iemand de formulering verbetert.
De oplossing is een eigen type:
check(" ")
class QuizError(Exception) is de hele definitie. Er is geen inhoud nodig behalve een docstring, omdat het de identiteit draagt: een aanroeper kan nu except QuizError schrijven en precies de fouten opvangen die dit programma bewust signaleert, en verder niets.
Een kleine familie
Eén exceptietype zegt ‘dit programma weigerde’. Een paar typen zeggen welk soort weigering:
QuestionNotFound erft van QuizError, dus except QuizError vangt die op. Dat levert de vervangbaarheid uit hoofdstuk 9 hier op: een aanroeper die om het onderscheid geeft vangt het specifieke type op, en een aanroeper die dat niet doet vangt de basis op en krijgt beide.
Dit is een kleine familie. Twee of drie typen onder één basis zijn bijna altijd genoeg. Een hiërarchie met vijftien exceptieklassen is een ontwerp dat niemand in het hoofd kan houden. Elke klasse moet haar plek verdienen doordat die ergens anders wordt opgevangen dan de andere klassen.
Waarom erft InvalidResponse van QuizError in plaats van rechtstreeks van Exception?
Wanneer je er geen bedenkt
Een eigen exceptie is het definiëren waard als een aanroeper waarschijnlijk deze wel en iets anders niet wil opvangen. Dat is de hele toets.
Definieer er geen als een ingebouwd type het al precies zegt. Een verkeerd type is overal een TypeError, in elk ooit geschreven Pythonprogramma. InvalidTypeError leert je aanroepers een eigen woord voor een algemeen idee.
Definieer er niet één per plek waar je opgooit. EmptyPromptError, BlankAnswerError en NegativePointsError zijn drie namen voor ‘deze vraag is ongeldig’ en niemand zal ze ooit afzonderlijk opvangen.
Een nuttige volgorde: begin met het ingebouwde type en stap over op een eigen type zodra je merkt dat je het afzonderlijk van iets anders moet opvangen.
Waar ze staan
Een exceptietype maakt deel uit van de publieke interface van een module: aanroepers moeten het noemen om het op te vangen. Zet typen waar aanroepers ze makkelijk kunnen bereiken. Voor de indeling uit hoofdstuk 10 betekent dat errors.py bovenaan het package, geïmporteerd door de domeinmodules die ze opgooien.
Daarom heeft de verplichte indeling van het afsluitende project ook een errors.py. Dat is geen archiefconventie; het is de plek waar een aanroeper kijkt om te ontdekken wat dit programma kan weigeren te doen.
Opdracht
Geef het quizprogramma een kleine exceptiefamilie en gebruik ingebouwde typen overal waar die het al zeggen.
Definieer QuizError(Exception) als basis, met InvalidResponse en QuestionNotFound eronder. Drie klassen, die elk alleen een docstring nodig hebben.
Gebruik ze daarna in QuestionBank:
question_at(index)gooitQuestionNotFoundop als de index buiten de bank ligt enTypeErrortenzij het type van de index exactintis.TrueenFalsezijn geen posities. Een verkeerd type is in elk Pythonprogramma eenTypeError, dus dat blijft zo.respond(index, response)gooitInvalidResponseop voor een leeg antwoord en vergelijkt anders het antwoord na het verwijderen van witruimte aan de uiteinden en het omzetten naar kleine letters. Geef de punten terug bij een overeenkomst of0bij een onjuist antwoord.
Ten slotte geeft run(bank, answers) een paar (score, refused) terug. Hier is refused een lijst met exceptieberichten. De functie vangt één keer per antwoord QuizError op en registreert elk bericht met str(error), wat het doel van de basis is. Die mag geen TypeError opvangen: een verkeerd type is een fout in de aanroepende code, geen weigering van dit programma.