c’est quoi ?¶
il s’agit d’un mécanisme pour gérer les situations exceptionnelles, comme par exemple
# on ne peut pas diviser par 0
1 / 0---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[1], line 3
1 # on ne peut pas diviser par 0
----> 3 1 / 0
ZeroDivisionError: division by zero# on ne peut pas ouvrir un fichier qui n'existe pas
with open("will-not-open.txt") as f:
pass---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Cell In[2], line 3
1 # on ne peut pas ouvrir un fichier qui n'existe pas
----> 3 with open("will-not-open.txt") as f:
4 pass
File ~/.local/lib/python3.12/site-packages/IPython/core/interactiveshell.py:343, in _modified_open(file, *args, **kwargs)
336 if file in {0, 1, 2}:
337 raise ValueError(
338 f"IPython won't let you open fd={file} by default "
339 "as it is likely to crash IPython. If you know what you are doing, "
340 "you can use builtins' open."
341 )
--> 343 return io_open(file, *args, **kwargs)
FileNotFoundError: [Errno 2] No such file or directory: 'will-not-open.txt'pourquoi ?¶
comme vous le voyez sur ces exemples, on ne peut pas vraiment gérer ces situation en retournant un code d’erreur:
à la rigueur dans le premier cas on pourrait imaginer renvoyer
math.nanou encoremath.infmais comment gérer le deuxième exemple ?
idéalement, on veut un mécanisme où le souci peut être réglé, soit par la fonction elle-même, ou sinon par l’appelant de la fonction, ou par l’appelant de l’appelant, etc..
(un peu comme dans un workflow humain, pensez: une banque; si le souci ne peut pas être réglé par votre conseiller, on va en parler au chef d’agence, à son chef, etc..)
exception et pile d’exécution (1)¶
dans ce genre de situations exceptionnelles, le langage va “lever une exception” (en anglais, lever = raise)
ça veut dire quoi ? voici ce qu’il va se passer:
dans le cas général on est dans une fonction qui a été appelée par une fonction qui a été appelée...
lorsqu’il y a exception, on commence par interrompre brutalement l’exécution
voyons pour commencer le cas où l’on n’a pas attrapé l’exception:
# une fonction qui va faire raise
# mais pas tout de suite
def time_bomb(n):
if n > 0:
return time_bomb(n-1)
else:
raise OverflowError("BOOM")# si on essaye de l'exécuter
# ça se passe mal
def driver():
time_bomb(1)
#
print("will never pass here")
driver()---------------------------------------------------------------------------
OverflowError Traceback (most recent call last)
Cell In[4], line 9
6 #
7 print("will never pass here")
----> 9 driver()
Cell In[4], line 5, in driver()
4 def driver():
----> 5 time_bomb(1)
6 #
7 print("will never pass here")
Cell In[3], line 6, in time_bomb(n)
4 def time_bomb(n):
5 if n > 0:
----> 6 return time_bomb(n-1)
7 else:
8 raise OverflowError("BOOM")
Cell In[3], line 8, in time_bomb(n)
6 return time_bomb(n-1)
7 else:
----> 8 raise OverflowError("BOOM")
OverflowError: BOOMl’instruction try .. except¶
comme vous pouvez le voir sur la première figure, on regarde dans la pile des appels, pour voir si à un moment donné on a attrapé l’exception; dans ce premier cas de figure, ça n’a pas été fait et le programme s’interrompt totalement (on retourne carrément dans le terminal !)
comment faire alors ?
c’est là qu’intervient l’instruction try .. except qui va nous permettre d’attraper l’exception
dans sa version la plus simple, elle se présente comme ceci:
# une instruction `try except` permet de capturer une exception
def divide(x, y):
try:
res = x / y
print(f"division OK {res=}")
except ZeroDivisionError:
print("zero divide !")
print("continuing... ")divide(8, 4)division OK res=2.0
continuing...
divide(8, 0)zero divide !
continuing...
exception et pile d’exécution (2)¶
voyons maintenant la logique de l’exception dans le contexte d’appels, éventuellement profonds
si on attrape l’exception, notre premier exemple devient ceci:
def driver_try():
try:
time_bomb(2)
print("not here")
except Exception as exc:
print(f"OOPS {type(exc)}, {exc}")
# et on passera bien ici
print("the show must go on")
driver_try()OOPS <class 'OverflowError'>, BOOM
the show must go on
l’instruction raise¶
pour compléter le tableau, on peut aussi signaler une condition exceptionnelle avec l’instruction raise
# je veux vérifier qu'on me passe bien ce que j'attends
# j'utilise l'exception prédéfinie 'ValueError', c'est exactement son propos
def set_age(person, age):
if not isinstance(age, int):
raise ValueError("a person's age must be an integer")
person['age'] = ageperson = dict()
set_age(person, '10')---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 3
1 person = dict()
----> 3 set_age(person, '10')
Cell In[9], line 6, in set_age(person, age)
4 def set_age(person, age):
5 if not isinstance(age, int):
----> 6 raise ValueError("a person's age must be an integer")
7 person['age'] = age
ValueError: a person's age must be an integerhiérarchie des exceptions¶
la clause raise doit fournir un objet idoine: ce doit être une instance de BaseException
par exemple on ne pourrait pas faire raise 1
la clause except¶
et du coup on utilise cette hiérarchie pour n’attraper qu’une partie des exceptions possibles
dans sa forme la plus générale, elle ressemble à ceci
# les différentes formes de except
try:
bloc
de code
except ExceptionClass: # les instances de
bloc # ExceptionClass
de rattrapage
except (Class1, .. Classn): # comme avec isinstance
...
except Class as instance: # donne un nom à l'objet
... # levé par raise
except: # attrape-tout - déconseillé
...où on voit que:
on peut mettre plusieurs
exceptaprès untry:chacune attrape une partie seulement des classesla première qui convient est la bonne, et on retourne à un régime non exceptionnel
si aucune ne convient: l’exception se propage dans la pile - comme si on n’avait pas mis le
try:
comment utiliser l’object exception¶
l’object exception (celui qu’on a donné à raise) contient généralement des informations utiles à mieux comprendre ce qu’il se passe
c’est pourquoi except est généralement utilisée sous sa forme except .. as qui permet d’inspecter le contenu
selon le type de l’exception, on va trouver les détails dans des attributs, et toujours au moins l’attribut args
exemple de except .. as¶
# imaginez que l'exception se produise au 4-éme appel dans la pile
# et que nous on n'a aucune idée du fichier qu'on est en train d'essayer d'ouvrir
try:
with open("inexisting-filename") as f:
...
except IOError as exc:
print(f"le type: {type(exc)}")
print(f"l'exception elle-même {exc}")
print(f"les arguments {exc.args}")
# si on veut aller plus loin on peut faire un peu d'introspection
# comme d'hab on va ignorer les attributs spéciaux
attributes = [symbol for symbol in dir(exc) if not '__' in symbol]
print(f"les attributs {attributes}")
print(f"du coup {exc.filename=} et {exc.strerror=}")le type: <class 'FileNotFoundError'>
l'exception elle-même [Errno 2] No such file or directory: 'inexisting-filename'
les arguments (2, 'No such file or directory')
les attributs ['add_note', 'args', 'characters_written', 'errno', 'filename', 'filename2', 'strerror', 'with_traceback']
du coup exc.filename='inexisting-filename' et exc.strerror='No such file or directory'
les exceptions sont efficaces¶
voici une manière décente pour ouvrir un fichier; le code est beaucoup plus concis et efficace
que de tester si le fichier existe, si ça n’est pas un répertoire, si on a les droits d’écriture, etc.
try:
with open('fichier-inexistant', 'r') as feed:
for line in feed:
print(line)
except OSError as err:
print(err)
print(err.args)
print(err.filename)[Errno 2] No such file or directory: 'fichier-inexistant'
(2, 'No such file or directory')
fichier-inexistant
notions avancées¶
try .. else¶
un peu comme avec for et while, on peut assortir le try d’une clause else
qui est exécutée uniquement s’il n’y a pas eu d’exception
def divide(x,y):
try:
res = x / y
except ZeroDivisionError:
print('zero divide !')
else:
print('all right, result is', res)
print('continuing... ')divide(8, 3)all right, result is 2.6666666666666665
continuing...
divide(8, 0)zero divide !
continuing...
try .. finally¶
une instruction try peut avoir une clause finally
cette clause est toujours exécutée
si il n’y a aucune exception
si il y a une exception attrapée
si il y a une exception non attrapée
et même s’il y a un
returndans le code !
elle sert à faire du nettoyage après l’exécution du bloc try
par exemple fermer un fichier
def finally_trumps_return(n):
try:
return 2 / n
finally:
print("finally is invicible !")finally_trumps_return(0)finally is invicible !
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[17], line 1
----> 1 finally_trumps_return(0)
Cell In[16], line 3, in finally_trumps_return(n)
1 def finally_trumps_return(n):
2 try:
----> 3 return 2 / n
4 finally:
5 print("finally is invicible !")
ZeroDivisionError: division by zeroinstruction raise¶
on n’a vu jusqu’ici la que sa forme usuelle raise instance
il existe aussi deux formes plus tarabiscotées:
raisetout court
c’est la forme usuelle pour propager depuis unexceptl’exception originale, qui reste intacteraise new_instance from original_excpour propagation avec modification
exception personnalisée¶
on peut bien souvent utiliser une des exceptions prédéfinies (comme ValueError ci-dessus)
mais parfois c’est intéressant de se définir ses propres exceptions
pour cela rien de plus simple:
dans la majorité des cas, on a uniquement besoin
d’un nom d’exception explicite finissant par
Errord’un message d’erreur
il suffit d’hériter de la classe
Exception(ou une de ses sous-classes bien entendu)par défaut, tous les arguments passés au constructeur
sont mis dans un attributargs(un tuple)
class SplitError(Exception):
pass
x, y = 1, 'a'
try:
raise SplitError('split error', x, y)
except SplitError as exc:
print(exc.args)('split error', 1, 'a')
le module traceback¶
en production, on devrait normalement s’astreindre à ne pas du tout utiliser d’attrape-tout
toutefois en développement, ce n’est pas évident de tout envisager du premier coup
aussi on trouve une forme assez répandue: attrape-tout avec instrumentation
import traceback
try:
# un gros code; difficile de dire
# a priori toutes les exceptions
# qui peuvent se produire
pass
except OSError as exc:
print(f"pour celle-ci je sais quoi faire {exc}")
except KeyboardInterrupt:
print("pour celle-ci aussi")
except:
# je suis tout près du main(), je ne veux pas laisser
# passer l'exception car ça se terminerait mal
import traceback
traceback.print_exc()# la même chose avec le module logging
# en vrai on ne fait jamais print()
import logging
logging.basicConfig(level=logging.INFO)
try:
# un gros code; difficile de dire
# a priori toutes les exceptions
# qui peuvent se produire
logging.info("in the code")
1/ 0
except OSError as exc:
logging.error(f"pour celle-ci je sais quoi faire {exc}")
except KeyboardInterrupt:
logging.info("pour celle-ci aussi: bye")
except:
# je suis tout près du main(), je ne veux pas laisser
# passer l'exception car ça se terminerait mal
logging.exception("exception inattendue")INFO:root:in the code
ERROR:root:exception inattendue
Traceback (most recent call last):
File "/tmp/ipykernel_2579/638466657.py", line 13, in <module>
1/ 0
~^~~
ZeroDivisionError: division by zero