0%

Fouten als onderdeel van programmaontwerp · oefening

Vertaal een fout van een lager niveau

Sommige fouten komen van onderaf met een bericht dat voor een ander publiek is geschreven.

Try it
parse_score("eight")

invalid literal for int() with base 10: 'eight'. Dat bericht gaat over int(). Degene die parse_score aanriep dacht aan een score en moet nu uitzoeken wat int() daarmee te maken heeft.

Laag opvangen, betekenisvol opgooien

De oplossing is de fout van het lagere niveau opvangen en een fout opgooien die de taal van de aanroeper spreekt:

Try it
parse_score("eight")

Nu gaat het bericht over scores, waar de functie ook over gaat. Dit is vertaling: een fout passeert een grens tussen twee niveaus van het programma en wordt onderweg opnieuw verwoord.

Het patroon verdient een naam omdat het veel verder gaat dan int(). Een functie die een bestand leest gooit ‘bestand niet gevonden’ op; de aanroeper wil ‘geen opgeslagen quiz voor die cursist’. Dezelfde gebeurtenis, andere woordenschat, en de aanroeper mag de tweede verwachten.

De oorzaak behouden

De versie hierboven laat de relatie impliciet, wat je kunt inspecteren:

Try it

__cause__ is None, maar de oorspronkelijke fout van int() is nog aanwezig als impliciete __context__. Python neemt normaal beide excepties op in de traceback met een verbindende tekst “During handling…”. Die automatische context is nuttig, maar zegt niet of de relatie een bewuste vertaling was of slechts een tweede fout binnen de afhandelaar.

raise ... from ... maakt die relatie expliciet:

Try it

Nu legt de exceptie het origineel expliciet vast in __cause__: jouw bericht is voor de lezer en het origineel eronder is voor degene die het moet oplossen. In een echte traceback drukt Python beide af, verbonden door “The above exception was the direct cause of the following exception”. Gebruik from om bewuste vertaling ondubbelzinnig te maken, niet omdat de impliciete context anders zou verdwijnen.

Wat voegt raise ValueError("...") from error toe ten opzichte van raise ValueError("...")?

Vertaal bij een grens, niet overal

Vertaling kost een try en een afhandelaar en is dus alleen de moeite waard waar de woordenschat echt verandert.

Wel de moeite waard om te vertalen. Een opslagfout die een domeinfout wordt. Een parsefout die ‘dit quizbestand is ongeldig’ wordt. Alles waarbij het bericht van het lagere niveau een hulpmiddel noemt waarvan de aanroeper nooit heeft gehoord.

Niet de moeite waard om te vertalen. Een fout doorgeven binnen één laag, waar alleen de formulering verandert. ValueError opnieuw opgooien als ValueError met dezelfde informatie voegt een frame toe, geen betekenis.

Er is ook een versie die echt schadelijk is:

try:
    return int(text)
except ValueError:
    return 0

Dat is geen vertaling, maar verhulling. Een ongeldige score wordt een geloofwaardige nul en het rapport van de cursist zegt dat die niets heeft gescoord in plaats van dat het bestand kapot is. Als je niet kunt zeggen wat een waarde betekent, verzin er dan geen.

Opnieuw opgooien zonder argument

Soms wil je op een fout reageren en die toch doorlaten. Een kale raise binnen een afhandelaar gooit opnieuw op wat je hebt opgevangen, met de traceback intact:

Try it

Registreren en afhandelen zijn verschillende beslissingen. Hiermee kun je het eerste doen zonder te doen alsof je het tweede hebt gedaan.

Opdracht

Drie functies lezen ruwe waarden en geven die aan code die in quizzen denkt. Elke functie moet falen in de woordenschat van de aanroeper, niet die van het hulpmiddel.

parse_points(text) geeft de punten terug als geheel getal. Bij tekst die niet kan worden geparseerd gooit die ValueError op met een bericht dat punten noemt en de ongeldige tekst toont, met het origineel als oorzaak.

lookup_question(bank, prompt) geeft de vraag terug die onder prompt is opgeslagen. Bij een ontbrekende vraagtekst gooit die ValueError op die de vraagtekst noemt, opnieuw met de oorspronkelijke KeyError als oorzaak. Een ontbrekende vraag is een ongeldig verzoek, geen ongeldige sleutel, en de aanroeper hoeft niet te weten dat de bank een dictionary is.

parse_row(parts) maakt een paar (prompt, points) uit een lijst met twee items zoals ["prompt", "3"]. Een rij met het verkeerde aantal onderdelen gooit ValueError op met een beschrijving ervan. Punten worden geparseerd met parse_points en die fout gaat rechtstreeks door: die spreekt al de juiste taal, dus er nog een fout omheen zetten zou een frame toevoegen en geen betekenis.

Ten slotte voegt audited_points(text, log) "bad points: <text>" aan het log toe en laat de fout daarna ongewijzigd doorgaan. Gebruik een kale raise.