Licence CC BY-NC-ND, Thierry Parmentelat & Arnaud Legout
from IPython.display import HTML
HTML(filename="_static/style.html")
exceptions#
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[2], 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[3], 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 ~/checkouts/readthedocs.org/user_builds/ue12-p24-python/envs/p24/lib/python3.12/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
317 if file in {0, 1, 2}:
318 raise ValueError(
319 f"IPython won't let you open fd={file} by default "
320 "as it is likely to crash IPython. If you know what you are doing, "
321 "you can use builtins' open."
322 )
--> 324 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:
lancé depuis le terminal
ça n’a pas une grande importance, mais pour être bien clair, dans ce qui suit, on suppose qu’on lance depuis le terminal un programme foo.py qui lance une fonction main() qui appelle driver()
tout ça c’est juste pour simuler une certaine profondeur dans les appels de fonction..
# 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[5], line 9
6 #
7 print("will never pass here")
----> 9 driver()
Cell In[5], line 5, in driver()
4 def driver():
----> 5 time_bomb(1)
6 #
7 print("will never pass here")
Cell In[4], 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[4], line 8, in time_bomb(n)
6 return time_bomb(n-1)
7 else:
----> 8 raise OverflowError("BOOM")
OverflowError: BOOM
l’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'] = age
person = dict()
set_age(person, '10')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[11], line 3
1 person = dict()
----> 3 set_age(person, '10')
Cell In[10], 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 integer
hié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
les sous-classes de BaseException
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
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:
attrape-tout ?
le fait de capturer toutes les exceptions - avec except: ou except Exception:
est généralement considéré comme une mauvaise idée!
il vaut mieux comprendre ce que l’on capture
car sinon on risque de rendre silencieuses d’autres exceptions qui pourtant méritent d’être traitées
on développe ce sujet un peu plus bas, cherchez “traceback”
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
le reste est pour les avancés
en première lecture vous pouvez vous arrêter ici, la suite couvre des aspects plutôt avancés de l’importation
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
xxx
une exception dans la clause else n’est pas capturée par les except précédents
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...
assez peu utilisé
en pratique toutefois, c’est peu utilisé
ici par exemple, on obtient exactement le même comportement
si on écrit le print('continuing...') à la fin du bloc try:
def divide(x,y):
try:
res = x / y
print('all right, result is', res)
except ZeroDivisionError:
print('zero divide !')
print('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[18], line 1
----> 1 finally_trumps_return(0)
Cell In[17], 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 zero
instruction 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)
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")
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 passec’est pourquoi
exceptest généralement utilisée sous sa formeexcept .. asqui permet d’inspecter le contenuselon le type de l’exception, on va trouver les détails dans des attributs, et toujours au moins l’attribut
argsexemple de
except .. as#