2.26. Lever des erreurs¶
Une fonction peut signaler un problème à son appelant en levant une exception. Le mot-clé est raise :
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
Appeler square_root(-1) s’arrête à la ligne raise, sort de la fonction et recherche un except correspondant chez l’appelant. Si aucun appelant ne l’intercepte, le script se termine par une trace d’appels.
2.26.1. Pourquoi lever plutôt que renvoyer une valeur sentinelle¶
Deux façons de signaler une « mauvaise entrée » :
# 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
La forme par exception est généralement préférable :
L’appelant doit délibérément traiter le cas d’erreur – soit avec un
try, soit en laissant l’exception se propager. Les sentinelles sont faciles à oublier et faciles à confondre avec un résultat normal.Le message d’erreur voyage avec l’exception ; l’approche par sentinelle doit attacher le diagnostic ailleurs.
Le comportement par défaut face à une exception non gérée est un plantage bruyant accompagné d’une trace d’appels qui pointe vers l’appel fautif. Les retours silencieux de
Nonese transforment plus tard en bugs subtils.
Ne recourez aux sentinelles que lorsque « introuvable » est un résultat courant et non exceptionnel – dict.get() renvoie None pour une clé absente précisément parce qu’il est attendu que les recherches échouent parfois.
2.26.2. Classes d’exception personnalisées¶
Pour lever un problème que l’appelant pourrait vouloir distinguer des erreurs intégrées, définissez une sous-classe de 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)
Un corps de class vide convient – c’est le nom lui-même qui importe, car les appelants interceptent par classe. Regroupez les erreurs apparentées sous une base commune si un appelant pourrait vouloir intercepter toute la famille en un seul bloc.
2.26.2.1. Re-lever¶
Un raise nu à l’intérieur d’un bloc except relève l’exception courante afin qu’elle se propage au gestionnaire suivant :
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
C’est la bonne forme lorsqu’une fonction veut observer une erreur (la journaliser, la compter, annuler une modification partielle) sans réellement la traiter.
2.26.3. Quand intercepter et quand propager¶
Une règle empirique utile :
Interceptez une exception au niveau capable de récupérer utilement – substituer une valeur par défaut, réessayer, ignorer la mauvaise entrée.
Laissez-la se propager lorsqu’il n’y a rien d’utile à faire sinon planter, ou lorsque la couche supérieure est celle qui sait comment récupérer.
Une fonction au milieu d’une pile d’appels qui avale toutes les erreurs et renvoie silencieusement rend les défaillances introuvables. Préférez laisser les exceptions voyager jusqu’à ce qu’elles atteignent du code qui a véritablement un plan.