Exceptions — afhandelen wat breekt
Eén specifieke fout vangen met try/except, waarom `except Exception: pass` bijna altijd fout is, `else` en `finally`, zelf fouten opwerpen, en een fout van de juiste grootte kiezen uit de familie.
Hoe wil je leren?
Het probleem dat we oplossen
De laatste vraag van het vorige hoofdstuk liet een programma drie regels correct verwerken en stoppen op de vierde. Kleiner ziet het er zo uit:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(Vóór de foutregel print Python ook enkele traceback-regels; in dit hoofdstuk wordt meestal alleen de laatste regel getoond.)
10 werd correct toegevoegd. 30 werd helemaal nooit toegevoegd. En print(total) draaide nooit.
Eén slechte regel stopte de hele klus. Tot nu toe hadden we twee opties — ofwel is elk stukje data perfect, ofwel stopt het programma.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40Er is een derde optie: laat het probleem gebeuren, en antwoord er daarna op.
Aan het einde van dit hoofdstuk kun je
- Eén specifieke fout vangen met
try/except - Uitleggen waarom een blote
except:ofexcept Exception: passbijna altijd fout is - De fout beetpakken met
as erren zijn bericht lezen - Uitleggen wat
elseenfinallydoen - Zelf fouten
raise'en, en bepalen wanneer - De familie van fouten herkennen, en er één van de juiste grootte vangen
Voorkennis: Bestanden lezen en schrijven.
Een fout is ook een object
Met as leg je de fout in je handen:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'De rode tekst die je al die tijd in de terminal zag, zijn deze objecten, en eentje printen geeft zijn bericht.
Er zit een praktische consequentie aan: de fout draagt de beste uitleg die er is. err printen is bijna altijd nuttiger dan in je eigen woorden te schrijven "er ging iets mis".
Vang de juiste fout
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'Een except vangt alleen het type dat hij noemt. Deze noemt TypeError, en er gebeurde een ValueError — dus het except-blok had er net zo goed niet kunnen staan.
Dat kan dwarsig voelen, en dat is precies het nuttige deel. De except-regel zegt welke fout je had voorzien — en wat je niet had voorzien, stopt nog steeds het programma, en dat is correct.
Daarom moet je dit niet schrijven:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40Het antwoord is goed en de code is nog steeds slecht. except Exception: pass slikt alles — de fouten waar je aan dacht en degenen waar je niet aan dacht. Wanneer numbers morgen per ongeluk een dictionary wordt, of een naam erin verkeerd is gespeld, geeft dit programma stilletjes een fout antwoord.
De regel: vang precies de fout waar je een plan voor hebt. En wanneer je hem hebt gevangen, doe iets — minstens: zeg wat er gebeurd is.
Meer dan één soort
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionMeerdere except-clausules kunnen elkaar opvolgen, en de eerste die matcht draait — de rest wordt er niet eens naar gekeken.
Wanneer het antwoord hetzelfde is, kun je ze samen schrijven:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ geeft de naam van het type fout — handig wanneer je logs schrijft.
Fouten zitten in families
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
TrueFouten zijn als een boom georganiseerd, en een ouder vangen vangt ook zijn kinderen:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneEen paar relaties die het weten waard zijn: FileNotFoundError en PermissionError zijn allebei OSError; KeyError en IndexError zijn allebei LookupError; en bijna alles is een Exception.
Waaruit de regel volgt: vang de kleinste fout die je kunt. except Exception is het makkelijkst te schrijven en het minst nuttig.
else en finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse draait wanneer er niets in het try-blok is stukgegaan. Het houdt de binnenkant van try klein — alleen de regel die kan falen — met het werk dat op succes volgt in else. Stop te veel in try en je vangt dingen die je nooit van plan was te vangen.
finally draait altijd, wat er ook gebeurd is. Zelfs na een return:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryDe return heeft de waarde al vastgesteld, maar finally draait voordat de functie wordt verlaten. Het is de plek om op te ruimen — hoewel with voor bestanden dat werk al heeft gedaan.
Zelf fouten opwerpen
Opwerpen is even belangrijk als vangen.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1Wat was het alternatief voor raise? None teruggeven, of 0 — en dan zou de fout reizen. Hij zou in een of andere som belanden en veel later tevoorschijn komen als een vreemd getal zonder herleidbare oorsprong.
Een fout stopt de bug waar hij geboren is, en zegt in zijn bericht waarom.
Wanneer je dat bericht schrijft, zet de schuldige waarde erin. "invalid quantity" en "quantity cannot be negative: -1" — alleen de tweede laat je meteen aan het werk gaan.
Een fout kan ook gevangen en opnieuw opgeworpen worden met een beter bericht. parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'Dit keer wordt de hele uitvoer getoond, want hier vertelt de laatste regel niet het hele verhaal. (Op jouw machine bevatten de File-regels het volledige pad naar het bestand; hier is het ingekort tot ....) Python printte twee tracebacks: de eerste is de originele fout van int(), de tweede is onze nieuwe ValueError. De regel ertussen, During handling of the above exception, another exception occurred:, zegt dat de tweede gebeurde terwijl de eerste werd afgehandeld. Er gaat geen informatie verloren — maar het leest alsof er een bug in het except-blok zit.
Wanneer je met opzet een nieuwe fout opwerpt, zeg dat dan: schrijf except ValueError as err: en daarna raise ValueError(f"not a number: {text!r}") from err. De middelste regel wordt dan The above exception was the direct cause of the following exception: — dat wil zeggen, "de eerste veroorzaakte de tweede", en dat is precies wat we bedoelen.
{text!r} voegt de waarde in via repr, dus de aanhalingstekens komen tevoorschijn — en het verschil tussen not a number: 'ten' en not a number: ten doet ertoe wanneer de waarde echt een lege tekst of een verdwaalde spatie is.
Eerst vragen, of achteraf sorry zeggen
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedBeide zijn goed. De tweede is gebruikelijker in Python, en met een reden: de eerste kijkt twee keer — één keer in in, één keer in [...] — en kan alsnog breken wanneer er tussendoor iets verandert.
Voor wat routine is, leest een if echter beter. Exceptions zijn voor het uitzonderlijke; de naam zegt het al.
Een compleet voorbeeld
orders.txt, met twee expres slechte regels:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2Vier dingen die het bekijken waard zijn.
parse_line werpt op en read_order vangt. De onderste functie heeft geen idee wat het programma als geheel moet doen — ze weet alleen dat deze regel onbruikbaar is. Beslissen wat daarmee moet gebeuren hoort bij de functie erboven. Fouten worden laag opgeworpen en hoog gevangen, en die verdeling is de hele taak van een exceptionsysteem.
Het regelnummer zit in het bericht. could not convert string to float: 'eight' zegt niet waar je moet zoeken; line 2: ... wel. Wat je op het moment van vangen weet en later niet meer zal weten, hoort in het bericht.
read_order breekt nooit. Hij geeft twee dingen terug — wat bruikbaar was en wat niet. Een ontbrekend bestand levert een antwoord van dezelfde vorm op, dus main heeft geen twee aparte routes nodig.
Het innerlijke try-blok is precies één regel lang. float(price) en int(quantity) — alleen het deel dat kan falen zit erin. parts = raw.split(",") staat erbuiten, want het zou niet mogen falen, en als het dat toch doet wil ik het weten.
Wanneer het breekt
De fout wordt niet gevangen, ook al is er een except Het type klopt niet. Het eerste woord van de laatste regel van het bericht is de naam van de fout — noem die, of vang zijn ouder.
Er is een except en het programma stopt alsnog De falende regel staat niet in het try-blok. Controleer de inspringing.
SyntaxError: expected 'except' or 'finally' block Er is een try geschreven zonder except. try kan niet alleen staan.
Alles "werkt" maar de antwoorden zijn fout Er zit ergens een except Exception: pass. Zoek hem op — daar zit het probleem.
Ik werp iets op maar er verschijnt geen bericht Er is raise ValueError geschreven in plaats van raise ValueError("..."). Voeg de haakjes en het bericht toe.
Een return in finally verliest de fout Klopt, en daarom moet je er geen schrijven. finally is alleen voor opruimen.
Stap 4 van 6 — Voorspellen
Test je begrip
Er is een except. Wat gebeurt er met het programma?
try:
int("twenty")
except TypeError:
print("caught")- A`caught` wordt geprint
- BEr wordt niets geprint en het programma eindigt rustig
- CHet stopt met een `TypeError`
- DHet programma stopt met een `ValueError` — de `except` had er net zo goed niet kunnen staan
De hele lus zit in het try. Wat wordt geprint?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
Zowel het try als het except heeft een return. Wat wordt geprint?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Ga terug naar summary.py van het vorige hoofdstuk, dat people.txt las.
Maak het nu zo dat het nooit crasht, en eindig door te zeggen welke regels overgeslagen zijn en waarom. Schrijf een parse_line(raw, number) die bij een slechte regel een ValueError opwerpt met het regelnummer erin.
Beschadig daarna het bestand met opzet — verwijder een komma uit één regel, zet letters waar een getal hoort in een andere, en laat één regel helemaal leeg. Bijvoorbeeld:
rafi,12
nadia 30
sami,ten
lina,25De lege regel is geen klacht — net als in het vorige hoofdstuk wordt hij stil overgeslagen. Het programma moet dus tot het einde lopen en twee klachten melden. Wanneer je de berichten schrijft zoals het complete voorbeeld doet (expected 2 fields, got ... en line {number}: {err}), is het einde van de uitvoer precies dit:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'Daarna vier experimenten:
- Vervang
except ValueErrordoorexcept Exception: pass. Wat is de uitvoer, en waarom is die gevaarlijker? - Verwijder het regelnummer uit het bericht van
parse_line. Hoe lang duurt het nu om een slechte regel te vinden? - Maak het
try-blok groter — zet het hele lichaam van de lus erin. Wat gebeurt er nu wanneer één regel slecht is? - Voeg een
finallytoe die na elke regel iets print, en controleer dat hij op de goede én de slechte regels draait.
Het derde experiment leert de nuttigste gewoonte van dit hoofdstuk: hoe kleiner het try-blok, hoe eerlijker de vangst.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken