2.26. Fouten opwerpen¶
Een functie kan een probleem aan zijn aanroeper signaleren door een uitzondering op te werpen. Het sleutelwoord is raise:
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
Het aanroepen van square_root(-1) stopt bij de raise-regel, springt uit de functie en zoekt naar een passende except in de aanroeper. Als geen enkele aanroeper het opvangt, eindigt het script met een traceback.
2.26.1. Waarom opwerpen in plaats van een schildwacht teruggeven¶
Twee manieren om “slechte invoer” te melden:
# signal with a sentinel
def square_root_or_none(x):
if x < 0:
return None
return x ** 0.5
# raise an exception
def square_root(x):
if x < 0:
raise ValueError("...")
return x ** 0.5
De uitzonderingsvorm is meestal beter:
De aanroeper moet de foutsituatie bewust afhandelen – ofwel met een
try, ofwel door de uitzondering te laten verspreiden. Schildwachten zijn gemakkelijk te vergeten en gemakkelijk aan te zien voor een normaal resultaat.Het foutbericht reist mee met de uitzondering; de schildwacht-aanpak moet de diagnose ergens anders aanhechten.
Het standaardgedrag bij een niet-afgehandelde uitzondering is een luide crash met een traceback die naar de aanstootgevende aanroep wijst. Stille
None-retourwaarden worden later subtiele bugs.
Gebruik schildwachten alleen wanneer “niet gevonden” een routinematige, niet-uitzonderlijke uitkomst is – dict.get() retourneert None bij een ontbrekende sleutel juist omdat van opzoekingen wordt verwacht dat ze soms missen.
2.26.2. Aangepaste uitzonderingsklassen¶
Om een probleem op te werpen dat de aanroeper misschien wil onderscheiden van ingebouwde fouten, definieer je een subklasse van Exception:
class ConfigError(Exception):
pass
def load_config(path):
try:
f = open(path)
except OSError as e:
raise ConfigError("missing config file: " + path)
try:
load_config("settings.json")
except ConfigError as e:
print("startup failed:", e)
De lege class-body is prima – de naam zelf is wat telt, omdat aanroepers per klasse opvangen. Groepeer verwante fouten onder een gemeenschappelijke basis als een aanroeper de hele familie in één blok zou willen opvangen.
2.26.2.1. Opnieuw opwerpen¶
Een kale raise binnen een except-blok werpt de huidige uitzondering opnieuw op, zodat deze zich naar de volgende afhandelaar verspreidt:
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
Dit is de juiste vorm wanneer een functie een fout wil waarnemen (loggen, tellen, een gedeeltelijke wijziging ongedaan maken) zonder deze daadwerkelijk af te handelen.
2.26.3. Wanneer opvangen en wanneer laten verspreiden¶
Een nuttige vuistregel:
Vang een uitzondering op het niveau op dat zinvol kan herstellen – vervang door een standaardwaarde, probeer opnieuw, sla de slechte invoer over.
Laat het verspreiden wanneer er niets nuttigs te doen is behalve crashen, of wanneer de laag erboven degene is die weet hoe te herstellen.
Een functie midden in een aanroepstack die elke fout inslikt en stilletjes terugkeert, maakt mislukkingen onnaspeurbaar. Geef er de voorkeur aan uitzonderingen te laten reizen totdat ze code bereiken die werkelijk een plan heeft.