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:
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:
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.

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.
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 detail | Waarom het helpt |
|---|---|
| Een specifiek type | zodat een aanroeper alleen dit kan opvangen |
| Een bericht dat de waarde noemt | zodat een lezer stopt met zoeken |
| Attributen die de waarden bevatten | zodat code kan reageren zonder tekst te ontleden |
| Een oorzaak bij vertaling | zodat 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.