0%

Fouten als onderdeel van programmaontwerp · oefening

Volg een fout door samenwerkende objecten

Een opgegooide exceptie stopt niet waar die is opgegooid. Die reist terug omhoog langs iedereen die op een antwoord wacht, totdat iets die opvangt of het programma eindigt.

In een programma met één functie is dat nauwelijks het vermelden waard. In een programma van samenwerkende objecten is het de nuttigste eigenschap van excepties.

De reis bekijken

Try it

Geef nu een getal waar tekst werd verwacht:

run_quiz(attempt, questions, [42])

De TypeError is opgegooid in Question.points_for. Niets in Attempt.answer of run_quiz noemt fouten en toch werden beide onderbroken. De traceback vermeldt elk ervan, met de binnenste als laatste:

  File "...", line 33, in run_quiz
    attempt.answer(question, response)
  File "...", line 27, in answer
    self.score = self.score + question.points_for(response)
  File "...", line 12, in points_for
    raise TypeError(f"a response must be text, not {type(response).__name__}")
TypeError: a response must be text, not int

Lees een traceback van onder naar boven. De laatste regel is wat er misging. Het frame erboven is waar. De frames daarboven laten zien hoe je daar kwam.

points_for gooit een exceptie op en noch answer noch run_quiz heeft een try. Wat gebeurt er met Attempt.answer?

Waarom dit nuttig is

Het midden van het programma zei niets over fouten en dat is het punt. Attempt.answer heeft één taak. Die ook laten beslissen wat er moet gebeuren bij elke mogelijke fout in een vraag zou het echte werk begraven onder afhandeling van problemen die de methode niet kan oplossen.

Een methode die een probleem niet kan oplossen hoort het niet op te vangen. De exceptie laten passeren is geen luiheid; het is de juiste afhandeling en het kost geen extra code.

Vergelijk dat met het alternatief dat programma’s zonder excepties moeten gebruiken:

def answer(self, question, response):
    earned, error = question.points_for(response)
    if error is not None:
        return None, error

    self.score = self.score + earned
    return earned, None

Elke functie geeft een waarde en een fout terug, elke aanroeper controleert en het echte werk vormt nu nog maar een derde van de code. De versie met excepties zegt hetzelfde door niets te zeggen.

Wat er meereist

Een exceptie draagt het type, het bericht en de hele keten van frames waar die doorheen ging. Dankzij die keten kun je een fout die vier objecten diep is opgegooid bovenaan nog diagnosticeren. Daarom is een exceptie inslikken kostbaar: die opvangen en doorgaan gooit de enige registratie van wat er gebeurde weg.

Het probleem van gedeeltelijke toestand

Er zijn echte kosten en hoofdstuk 5 heeft je er al op voorbereid. Wanneer een exceptie een methode halverwege onderbreekt, blijft alles wat die al heeft gedaan uitgevoerd:

Try it

Een geregistreerd antwoord zonder score ervoor. Er werd aan de lijst toegevoegd, daarna kwam de exceptie en het object is nu inconsistent.

De oplossing is de regel uit hoofdstuk 5 en dit is waarom die regel ertoe deed: doe eerst alles wat kan mislukken en verander daarna de toestand. De twee regels omdraaien maakt deze methode veilig.

De volgende les bepaalt wie opvangt en waar.

Opdracht

Maak Attempt.answer veilig tegen een onderbreking. Die registreert nu het antwoord voordat die de vraag om punten vraagt, waardoor een fout een antwoord zonder bijbehorende score achterlaat. Verander de volgorde zodat alles wat kan mislukken gebeurt voordat er toestand verandert.

Voeg geen try toe aan Attempt of aan Question. Geen van beide kan een ongeldig antwoord herstellen, dus geen van beide hoort iets op te vangen.

De gegeven helper trace_of(action) doorloopt error.__traceback__ en geeft de functienamen terug waar een exceptie doorheen ging. Lees die en voer die uit om de traceback concreet te maken, maar bewerk de helper niet; rechtstreeks tracebacks manipuleren is niet de taak van de cursist.