Werken met uitzonderingen

Inleiding

In dit hoofdstuk leer je werken met uitzonderingen. Je zult in je programma’s vaak uitzonderingen tegenkomen. Uitzonderingen zijn situaties die optreden als iets - al dan niet verwacht - fout gaat. Bijvoorbeeld: een functie waarmee je twee getallen deelt, zal een uitzondering opwerpen als je probeert te delen door 0. Als je in de vorige hoofdstukken fouten tegenkwam, dan stopte je programma. In dit hoofdstuk leer je hoe je fouten netjes afhandelt.

Leerdoelen

Aan het einde van dit hoofdstuk:

  • Begrijp je wat een uitzondering is in Python

  • Begrijp je hoe je uitzonderingen gebruikt om het programmaverloop te bepalen

  • Begrijp je hoe je uitzonderingen afvangt en opwerpt

Werken met uitzonderingen

Als je programmeert kunnen er fouten ontstaan. Veel van dit soort fouten zijn gelukkig te voorspellen zodat je ze van tevoren al kunt voorkomen of afhandelen. Zo’n fout onderbreekt als het ware je programmaverloop, en wordt daarom vaak een uitzondering genoemd. In deze paragraaf leer je werken met dergelijke uitzonderingen.

Wat is een uitzondering?

Wat een uitzonderlijke situatie is, is contextafhankelijk en is ook een kwestie van perceptie. Een voorbeeld kan zijn dat je een bestand probeert te openen dat niet bestaat. In je programma ga je er vanuit dat het bestaat, misschien omdat je het zelf hebt aangemaakt. Als het dan toch niet bestaat, is dat met recht een uitzonderlijke situatie te noemen.

Bekijk maar eens wat er gebeurt als je een niet-bestaand bestand wilt open:

f = open("ik-besta-niet.txt", mode="rt", encoding="utf-8")


Traceback (most recent call last):
  File "/home/erwin/programmeren-met-python/main.py", line 1, in <module>
    f = open("ik-besta-niet.txt", mode="r", encoding="utf-8")
FileNotFoundError: [Errno 2] No such file or directory: 'ik-besta-niet.txt'

Voer je dit uit in Thonny, dan zie je in de shell een soortgelijke traceback als in dit voorbeeld. Het belangrijkste zie je op regel 7: FileNotFoundError. Dit maakt duidelijk welke fout er is ontstaan: het bestand is niet gevonden. Voor meer informatie lees je deze traceback helemaal. Zo lees je op regel 5 dat de fout zich voordeed op regel 1 in het bestand main.py. Op regel 6 lees je welke stuk code de fout heeft veroorzaakt.

Deze FileNotFoundError is één van de vele uitzonderingen - Exceptions in het Engels - die Python rijk is.

In de documentatie van Python lees je welke ingebouwde exceptions er allemaal zijn.

Een paar voorbeelden:

IndexError

Als je een index probeert op te halen die niet bestaat.

KeyError

Als de opgegeven sleutel niet bestaat in een dictionary.

ValueError

Als het opgegeven argument een niet-geldige waarde heeft, bijvoorbeeld als je de wortel van een negatief getal wilt trekken: math.sqrt(-4).

Opdracht 1: IndexError

Schrijf code dat bewust een IndexError veroorzaakt.

Klik om het antwoord te tonen

Een uitwerking is:

mijn_lijst = [1, 2, 3]
print(mijn_lijst[3])

De laatste index van deze specifieke lijst is 2 (de index begint bij 0, weet je nog?) mijn_lijst[3] zal dus niet werken.

Uitzonderingen om het programmaverloop te bepalen

Tref je een uitzondering in je programma, dan is het niet erg fraai voor de gebruiker als heel je programma stopt en een - voor de gebruiker - cryptische traceback opwerpt.

Het is mooier om dergelijke exceptions af te handelen en als ze voorkomen een andere richting in je programma te kiezen. Het is hiermee vergelijkbaar met if-else: als dit, dan dat. Als het bestand bestaat, open het dan. Anders maken we het aan.

Wanneer gebruik je dan if-else en wanneer try…​except…​? Daar zijn geen harde regels voor, maar het wordt als 'Pythonisch' beschouwd om veel met try…​except…​ te werken. Een bekende uitrdukking hierin is: "It’s easier to ask for forgiveness than permission", afgekort tot EAFP. De tegenhanger is LBYL: "Look before you leap".

De voorkeursstijl in Python is om uit te gaan van de 'happy flow', en actie te ondernemen als je aannames niet kloppen (EAFP). Hier past try…​except…​ dus goed bij. De andere stijl is om van tevoren alles proberen af te dekken, met if-else (LBYL).

Maar if-else is met name bedoeld om keuzes te maken op basis van condities: als de gebruiker A typt, doe dan x, anders y. try…​except…​ is meer bedoeld voor die situaties waar de keuze al is gemaakt (open dit bestand), en waar je verwacht dat het meestal ook goed gaat. Gaat het toch fout, dan handel je het af.

Uitzonderingen afvangen

Dit afhandelen van uitzonderingen doe je met try.. except .., ofwel, je probeert iets en als dat niet lukt (er ontstaat een fout), dan doe je wat anders.

try:
    # Open het configuratiebestand
    with open("config.txt", mode="rt", encoding="utf-8") as f:
        print("Je configuratie is...")
except FileNotFoundError:
    print("Configuratie bestaat niet, maak een standaard configuratie aan.")
    with open("config.txt", mode="wt", encoding="utf-8") as f:
        f.write("Standaard configuratie")

        print("Je configuratie is...")

# De rest van je code

Op regel 1 start je met try:, in het blok eronder staat de code die je wilt uitvoeren. Bestaat het bestand niet, dan wordt dit afgevangen door de except FileNotFoundError: op regel 5. Je belandt dan in het tweede blok, waar je het bestand aanmaakt en er iets in zet.

Je programma gaat nu netjes door, ook als het bestand per ongeluk niet bestaat. Maar wat als er een ander type fout ontstaat? Bijvoorbeeld, het bestand bestaat wel maar staat op een plek waar je programma geen leesrechten voor heeft. Dan stop je programma alsnog.

Verwacht je dat deze uitzondering kan voorkomen, dan kun je die ook afvangen.

try:
    # Open het configuratiebestand
    with open("config.txt", mode="rt", encoding="utf-8") as f:
        print("Je configuratie is...")
except FileNotFoundError:
    print("Configuratie bestaat niet, maak een standaard configuratie aan.")
    with open("config.txt", mode="wt", encoding="utf-8") as f:
        f.write("Standaard configuratie")

        print("Je configuratie is...")
except PermissionError:
    print("Ik kon het configuratiebestand helaas niet lezen. "
          "Pas de rechten van het systeem aan.")

# De rest van je code

Op regel 11 zie je dat er nu ook wordt gecontroleerd op PermissionError. Je kunt net zoveel except-blokken toevoegen als nodig is.

Uitzonderingen opwerpen

In bovenstaande code handel je de fouten af en gaat je programma verder. Er zijn ook situaties waarin je niet weet hoe je de fout moet afhandelen. Stel dat je het in inlezen van een configuratiebestand, zoals hierboven, in een functie hebt geplaatst. Deze functie roep je op verschillende plaatsen in je code aan. Afhankelijk van waar het wordt aangeroepen wil je dat je programma doorgaat, stopt of een waarschuwing geeft.

De functie weet dus niet hoe het de fout moet afhandelen. In dat geval kun je de exception opnieuw opwerpen.

def read_config():
    try:
        # Open het configuratiebestand
        with open("config.txt", mode="rt", encoding="utf-8") as f:
            print("Je configuratie is...")
    except (FileNotFoundError, PermissionError):
            raise

De aanroeper van read_config kan nu zelf bepalen hoe het de fout afhandelt. Overigens zie je dat je op regel 6 op meerdere exceptions tegelijk kunt controleren. De raise zal de uitzondering die is opgetreden opnieuw opwerpen.

In dit geval is de try…​except…​ niet heel nuttig, omdat de aanroeper ook een try…​except…​ zal gebruiken. Het wordt nuttiger als je voordat je de uitzondering opnieuw opwerpt eerst nog een actie uitvoert. Ook kan het nuttig zijn in de functie op verschillende uitzonderingen te controleren en vervolgens een enkele uitzondering op te werpen.

# Definieer een eigen uitzondering
class MijnUitzondering(Exception):
    pass


# Definieer de functie om het configuratiebestand te openen
def read_config():
    try:
        # Open het configuratiebestand
        with open("config.txt", mode="rt", encoding="utf-8") as f:
            print("Je configuratie is...")
    except (FileNotFoundError, PermissionError) as e:
        # Log de fout
        print("Fout gelogd")

        # Werp een eigen uitzondering op
        raise MijnUitzondering(f"Er ging iets niet goed: {e}")


# Roep de functie aan
try:
    read_config()
except MijnUitzondering as e:
    # Handel de fout op de juiste manier af
    print(e)

Op regel 1 maak je een eigen uitzondering aan door een nieuwe class te maken die overerft van Exception. De uitzondering hoeft verder niets te doen.

In de functie controleer je op regel 12 op verschillende uitzonderingen. Nieuw is het stukje as e. Dit maakt het mogelijk om de uitzondering die optreedt opnieuw te gebruiken.

Op regel 14 log je de fout en op regel 17 werp je niet de originele uitzondering op, maar je eigen uitzondering. Het foutbericht bevat het originele foutbericht, die je krijgt door e mee te geven.

Roep je nu de functie aan, dan kun je controleren op MijnUitzondering en het op de juiste manier afhandelen.

Opdracht 2: Uitzondering

Wat verwacht je dat print(e) op regel 25 toont?

Klik om het antwoord te tonen

Omdat je eerder raise MijnUitzondering(f"Er ging iets niet goed: {e}") hebt gebruikt, zal de e op regel 25 die uitzondering met dat bericht bevatten. In dit geval bestaat config.txt niet en wordt de complete foutmelding dus:

Er ging iets niet goed: [Errno 2] No such file or directory: 'config.txt'

Opruimen

Het kan nodig zijn een bepaalde actie uit te voeren, ongeacht of de code in het try-blok wel of niet is gelukt. Een voorbeeld kan zijn om een geopend bestand altijd te weer te sluiten (ter illustratie, in de praktijk zul je hiervoor with gebruiken).

try:
    f = open("ik-besta-niet.txt", mode="r", encoding="utf-8")
    # Doe iets met de waarden in het bestand
except ValueError as e:
    print(e)
    raise
finally:
    f.close()

Ook al bevat het bestand nu een waarde waar je niets mee kunt en wordt er een ValueError opgeworpen, het bestand zal eerst nog gesloten worden in het finally-blok.