0%

Fouten als onderdeel van programmaontwerp · oefening

Behoud nuttige diagnostische informatie

Het bericht van een exceptie wordt één keer geschreven en onder druk gelezen. Deze les zorgt ervoor dat het nog iets nuttigs zegt tegen de tijd dat iemand het ziet.

Draag de waarden mee, niet alleen de woorden

Een bericht is een string. Een aanroeper die iets met de details wil doen moet ze daar weer uit halen. Geef de exceptie in plaats daarvan attributen:

Try it

super().__init__(...) geeft het bericht naar boven door, zodat de exceptie afdrukken blijft werken. De twee attributen zijn er voor code die de getallen wil. Een afhandelaar kan nu zeggen ‘je vroeg om 9, probeer 0 tot en met 1’ zonder een zin uit elkaar te halen.

Dit is precies de moeite waard wanneer een aanroeper de waarden nodig heeft. Als niets ooit error.index leest, is alleen het bericht prima.

Verlies de oorzaak niet

Les 4 introduceerde raise ... from .... Hier is dat het belangrijkst, omdat het verschil pas zichtbaar wordt als er iets is misgegaan:

Try it

Zonder from is __cause__ None, maar Python legt het origineel nog steeds automatisch vast in __context__ en toont normaal beide excepties met een verbindende tekst “During handling…”. Met from is het origineel ook de expliciete __cause__ en noemt de traceback het de directe oorzaak. Die expliciete relatie vertelt hulpmiddelen en lezers dat de vertaling bewust was.

Impliciete context kan voortkomen uit bewuste vertaling zonder from of uit een onbedoelde fout binnen een afhandelaar. Een expliciete oorzaak neemt die dubbelzinnigheid weg. Het diagram noemt de twee kanten ‘Implicit context’, impliciete context, en ‘Explicit cause’, expliciete oorzaak. ‘New QuizError’ is de nieuwe QuizError en ‘original ValueError’ de oorspronkelijke ValueError. ‘Intentional translation’ betekent bewuste vertaling. De onderste tekst zegt links dat tijdens het afhandelen een andere exceptie optrad en rechts dat de bovenstaande exceptie de directe oorzaak was.

Twee panelen met exceptieketens. Bij impliciete context bewaart het opgooien van QuizError binnen een ValueError-afhandelaar de ValueError in __context__ en zegt de traceback dat er tijdens het afhandelen een andere exceptie optrad. Bij een expliciete oorzaak bewaart het opgooien van QuizError vanuit de opgevangen fout de ValueError in __cause__ en noemt die de directe oorzaak.

Waarom geef je de voorkeur aan raise QuizError(...) from error boven raise QuizError(...)?

De drie manieren waarop informatie verloren gaat

Inslikken. except QuizError: pass wist de gebeurtenis. Niets weet dat die plaatsvond.

Afvlakken. except QuizError as error: raise QuizError("something went wrong") vervangt het bovenste bericht door een vaag bericht. Het origineel blijft normaal als context in de traceback bestaan, maar aanroepers ontvangen nu een minder nuttige exceptie zonder de oorspronkelijke attributen.

Loggen in plaats van afhandelen. Een fout registreren en daarna doorgaan alsof die niet is gebeurd is hetzelfde als inslikken, met een papieren spoor dat niemand leest. Als je logt en niet veilig verder kunt, gooi dan opnieuw op.

Try it

De kale raise houdt de actieve exceptie en de traceback intact. raise error gooit hetzelfde object opnieuw op, maar voegt de huidige plek aan de traceback toe. raise QuizError(str(error)) maakt daarentegen een andere exceptie en verliest gestructureerde details tenzij je die bewust opnieuw opbouwt.

Hoe een goede fout eruitziet

Alles bij elkaar heeft een diagnosticeerbare fout vier dingen:

Diagnostisch detailWaarom het helpt
Een specifiek typezodat een aanroeper alleen dit kan opvangen
Een bericht dat de waarde noemtzodat een lezer stopt met zoeken
Attributen die de waarden bevattenzodat code kan reageren zonder tekst te ontleden
Een oorzaak bij vertalingzodat de onderliggende gebeurtenis behouden blijft

Niet elke fout heeft alle vier nodig. Een ValueError voor een lege vraagtekst heeft de eerste twee nodig en verder niets. Het punt is dat elk een beslissing is. De standaard ‘gooi iets op en hoop’ neemt alle vier slecht.

De oefening bouwt een exceptie die de context meedraagt en een grens die daarvan gebruikmaakt.

Opdracht

Maak de fouten diagnosticeerbaar.

QuestionNotFound(index, available) bewaart beide waarden als attributen en geeft een bericht naar boven door met super().__init__, zodat afdrukken no question at 9; this bank has 2 oplevert.

InvalidResponse(prompt, response) doet hetzelfde en bewaart prompt en response, met een bericht dat beide noemt.

load_points(raw) vertaalt de fout van int() naar QuizError en behoudt het origineel als oorzaak.

describe(error) zet een opgevangen exceptie om in "<TypeName>: <message>" en voegt " (caused by <CauseTypeName>)" toe als er een oorzaak is. De functie leest __cause__, niet de berichttekst.

collect(bank, answers, log) is de grens. Voor elke weigering voegt die describe(error) aan het log toe en gaat verder; de functie geeft de score terug. Een TypeError is geen weigering, dus die wordt gelogd en daarna opnieuw opgegooid met een kale raise, zodat de oorspronkelijke traceback intact blijft.