Fouten als onderdeel van programmaontwerp · oefening
Vang fouten op bij de interfacegrens
Als geen enkele methode onderweg een exceptie hoort op te vangen, moet iemand dat uiteindelijk toch doen. De vraag die deze les beantwoordt is wie.
De regel
Vang op waar je er iets aan kunt doen.
Dat is niet waar de fout gebeurde en meestal ook niet in de buurt. Het is waar het programma weet wat de fout betekent voor de gebruiker. In een terminalprogramma is dat het startpunt. Op een webpagina is het de verzoekafhandelaar. In een test is het de testrunner.
Die plekken hebben een naam: de grens. Daarbinnen zit je domein, dat berekent en weigert. Daarbuiten is een persoon, die een zin nodig heeft in plaats van een traceback. In het diagram betekent ‘Domain’ domein en ‘Boundary’ grens. Het domein kent de gebruiker niet (‘does not know the user’), de exceptie gaat verder (‘propagates’) en de quiz geeft die door (‘passes it on’). Bij de grens staat: roep domeincode aan (‘call domain code’), vang TypeError op (‘catch TypeError’) en druk een bruikbaar bericht af (‘print useful message’).

Het ongeldige antwoord leverde een leesbare regel op en de quiz ging verder. Question weet nog steeds niets over mensen, afdrukken of doorgaan en hoefde niet te veranderen.
Specifiek opvangen
Twee regels houden een afhandelaar duidelijk.
Vang het meest specifieke type op dat je echt kunt afhandelen. except TypeError zegt dat je over TypeError hebt nagedacht. except Exception zegt dat je het opgaf en zal ook de AttributeError van je eigen typefout inslikken.
Schrijf nooit een kale except: zonder iets erachter. Die vangt ook KeyboardInterrupt op en kan daardoor het gebruikelijke toetsenbordverzoek om het programma te stoppen inslikken.
Er is één plek waar breed opvangen juist is: de buitenste afhandelaar van een langlopend programma, die onverwachte fouten moet loggen en verder moet blijven werken. Zelfs daar gooit die opnieuw op of registreert die, en gaat die nooit stilzwijgend verder.
Een Quiz.score() roept Question.points_for aan, dat TypeError kan opgooien. Waar hoort de try?
Inslikken
Wat misgaat bij opvangen is niet te weinig opvangen. Het is dit:
try:
score = score + question.points_for(response)
except Exception:
pass
Het programma gaat verder met een score die ongemerkt verkeerd is en elk spoor van de oorzaak is verdwenen. Dit is erger dan crashen, omdat een crash wordt hersteld.
Als je iets opvangt, doe dan minstens één van deze dingen: vertel het iemand, registreer het, gebruik een verdedigbare terugvaloptie of gooi opnieuw op. pass is alleen te verdedigen als de exceptie echt betekent ‘hier is niets te doen’. Dan verdient dat commentaar dat dit uitlegt.
Opruimen met finally
Soms moet iets gebeuren ongeacht of er een fout was:
scored_run(questions, ["def", 42])
Het finally-blok werd beide keren uitgevoerd, ook bij het verlaten na de fout. Daarvoor is het bedoeld: iets vrijgeven, iets sluiten of registreren dat er een poging plaatsvond, ongeacht de uitkomst.
finally handelt de fout niet af. De exceptie gaat daarna verder, wat meestal juist is: opruimen en beslissen wat een fout betekent zijn verschillende taken.
De oefening zet de grens op één plek en laat het domein onaangeraakt.
Opdracht
Zet de afhandeling bij de grens en nergens anders.
run_quiz(questions, responses) is de grens. Die geeft een paar (score, problems) terug, waarbij problems een lijst met strings "<prompt>: <message>" is, één per antwoord dat moest worden overgeslagen. Een ongeldig antwoord wordt overgeslagen en de quiz gaat verder.
Vang specifiek TypeError en ValueError op. Vang geen Exception op: een typefout in je eigen code moet nog steeds duidelijk crashen in plaats van als een verkeerd antwoord van de cursist te worden gemeld.
Question.points_for en Attempt.record blijven precies zoals ze zijn. Geen van beide kan bepalen wat een fout betekent, dus geen van beide mag een try krijgen.
De gegeven audited_run toont de afzonderlijke rol van finally: die registreert hoeveel antwoorden zijn onderzocht, of de uitvoering nu terugkeert of een onverwachte fout naar buiten doorgaat. Lees die en voer die uit, maar houd je implementatietaak gericht op run_quiz.