XIX. Les erreurs et les exceptions▲
XIX-1. Préambule▲
Depuis l'avènement des langages de haut niveau, on fait maintenant la distinction entre « erreurs » et « exceptions ».
Une erreur concerne principalement la syntaxe. Elle se produit une seule fois lors de l’analyse syntaxique du programme et, une fois corrigée, disparaît à jamais. Elle peut aussi concerner la logique du code par rapport au résultat attendu, mais dans ce cas, elle ne se révèle pas à l’exécution.
Une exception concerne un événement non attendu ou un élément incorrect survenant durant l'exécution du programme et provoquant un comportement erratique à ce moment‑là. Sa caractéristique principale est son côté fortuit, voire aléatoire, lié au fait qu’on ne peut jamais vraiment tout contrôler surtout quand les données proviennent d’un flux externe au programme.
2.
3.
4.
>>>
1
/
0
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
ZeroDivisionError
: integer division or
modulo by zero
Une exception, une fois levée, remonte toute la chaîne des instructions qui l'ont amenée pour arriver finalement à l'OS qui exécute le programme.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
def
a
(
): 1
/
0
...
>>>
def
b
(
): a
(
)
...
>>>
b
(
)
Traceback (
most recent call last):
File "<stdin>"
, line 3
, in
<
module>
File "<stdin>"
, line 2
, in
b
File "<stdin>"
, line 1
, in
a
ZeroDivisionError
: integer division or
modulo by zero
Cet historique se lit de bas en haut. Donc, dans cet exemple, il y a une exception ZeroDivisionError
qui se produit à la ligne 1 dans la fonction a. Cette fonction a est appelée à la ligne 2 dans la fonction b. Et cette fonction b est appelée à la ligne 3 du module (programme principal).
L'affichage de l'historique des appels est important, car la cause d'une exception ne se produit pas forcément là où l'exception est levée. Ainsi, dans l'exemple précédent, la cause est effectivement là où l'exception se produit, mais dans le code suivant…
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
def
a
(
n): 1
/
n
...
>>>
def
b
(
): a
(
0
)
...
>>>
b
(
)
Traceback (
most recent call last):
File "<stdin>"
, line 3
, in
<
module>
File "<stdin>"
, line 2
, in
b
File "<stdin>"
, line 1
, in
a
ZeroDivisionError
: integer division or
modulo by zero
… l'exception et les messages sont exactement les mêmes, pourtant, si on analyse le code, on se rend compte que la cause de l'exception n'est pas dans la fonction a qui se contente d'utiliser ce qu'on lui passe pour travailler, mais dans la fonction b qui appelle a avec un mauvais paramètre.
XIX-2. Se prémunir▲
La première façon de se prémunir contre les exceptions consiste généralement à les détecter avant qu'elles ne se produisent. Cependant, il existe des situations où cette détection est trop compliquée, voire impossible.
La seconde façon consiste alors à englober le code « sensible » dans un bloc try
…except
. Les instructions du bloc seront exécutées, mais si une exception se produit durant cette exécution ; et si cette exception a été prévue dans un cas except
, alors les instructions situées dans ce cas except
seront exécutées, mais l’exception sera neutralisée et le programme continuera son travail.
Il est possible de gérer plusieurs exceptions de façon distincte…
… ou de gérer d'un coup plusieurs exceptions en les regroupant dans un tuple.
2.
3.
4.
5.
6.
7.
def
calcul
(
n): return
n **
0.5
/
n
try
:
x=
input(
"Entrez un nombre : "
)
print
(
calcul
(
x))
except
(
ValueError
, ZeroDivisionError
) as
e:
print
(
"Désolé, ce calcul est impossible sur
%s
– Exception
%s
"
%
(
x, e))
XIX-3. Les instructions « else » et « finally »▲
Le bloc try
contient deux autres instructions possibles.
L'instruction else
va s'exécuter si aucune exception n'a été levée.
C'est relativement équivalent à ceci :
Toutefois, rajouter un bloc else
permet de faire la différence entre ce qui est vraiment sensible et ce qui ne l’est pas ; tout en gardant l’ensemble à l’esprit. Ainsi, dans cet exemple, les affichages étant secondaires par rapport aux tentatives de calcul qui, elles, sont le cœur de l’opération, ils ont fortement intérêt à être déportés dans un bloc else
.
L'instruction finally
permet de rajouter un bloc d'instructions qui s'exécutera dans tous les cas.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
calcul
(
n): return
n **
0.5
/
n
try
:
x=
input(
"Entrez un nombre : "
)
res=
calcul
(
int(
x))
except
(
ValueError
, ZeroDivisionError
) as
e:
print
(
"Désolé, ce calcul est impossible sur
%s
– Exception
%s
"
%
(
x, e))
else
:
print
(
"Le résultat du calcul de
%d
est
%d
"
%
(
x, res))
finally
:
print
(
"Quoi qu'il arrive, ceci sera exécuté"
)
Cela permet généralement de libérer des ressources éventuellement allouées avant ou dans le try
.
À noter que le bloc finally
s’exécute même si on quitte le try
avant de l’atteindre.
2.
3.
4.
5.
6.
7.
8.
9.
def
toto
(
):
try
:
return
"Hello"
finally
:
print
(
"fin"
)
>>>
toto
(
)
fin
'Hello'
XIX-4. L’instruction « raise »▲
L'instruction raise
permet à l’utilisateur de lever lui‑même l’exception de son choix. Mais il ne peut lever qu’une exception faisant partie de l’arbre des exceptions Python (qui commence à BaseException
), quitte à lui ajouter éventuellement ensuite un message personnalisé. Dans ce dernier cas, le message doit être placé entre parenthèses (Python 2 autorise aussi la virgule).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
calcul
(
n):
if
not
isinstance (
n, (
int, float)):
raise
ValueError
(
"Ce calcul ne peut se faire qu’avec
%s
numérique"
%
n)
if
n <
0
:
raise
ValueError
(
"On ne calcule pas de racines carrées sur
%s
négatif"
%
n)
return
n **
0.5
/
n
try
:
x=
input(
"Entrez un nombre : "
)
print
(
calcul
(
int(
x)))
except
(
ValueError
, ZeroDivisionError
) as
e:
print
(
"Désolé, ce calcul est impossible sur
%s
– Exception
%s
"
%
(
x, e))
XIX-5. Assertion▲
L'assertion est un mécanisme permettant de contrôler la validité d’une expression avec l’instruction assert
.
Si l’expression est vraie, l’instruction ne fait rien, sinon elle renvoie une exception AssertError qui peut être agrémentée d’un message personnalisé si celui‑ci est placé à la suite de l’instruction.
2.
3.
4.
def
inverse
(
n):
assert
isinstance(
n, (
int, float)), "
%s
non numérique"
%
n
assert
n!=
0
, "Pas d’inverse possible pour 0"
return
1
/
n
Le but de cette instruction est généralement de détecter les erreurs de programmation dans la phase de développement et non dans la phase d’utilisation. D’autant plus que l’exécution du programme en mode « optimisé » (option ‑O) ignore alors toutes les assertions présentes dans le code.
XIX-6. Arbre des exceptions▲
L’ensemble possible des exceptions est organisé en arbre d’héritage à partir d’une exception racine BaseException
.
Python 3 |
Python 2 |
---|---|
Sélectionnez
|
Sélectionnez
|
À noter que positionner une gestion d'exception sur l'exception X détectera aussi toutes les exceptions situées sous la branche X. De plus, les exceptions du plus haut niveau (situées au début de BaseException
) intègrent les interruptions système comme le ctrl+c clavier (KeyboardInterrupt
). Ainsi, le programmeur qui veut continuer à avoir la main sur son programme n'a pas vraiment intérêt à monter aussi haut. La première exception algorithmiquement utile pour lui est donc Exception
dont toutes les autres exceptions pouvant survenir en programmation héritent. C’est aussi l’exception qui est prise par défaut quand elle n’est pas spécifiquement indiquée dans l’instruction Except.
XIX-7. Définir ses propres exceptions▲
Les exceptions étant elles aussi des objets ; rien n'interdit alors d'en hériter pour définir ses propres exceptions.
XIX-8. Pardon ou permission▲
Généralement, le programmeur consciencieux a naturellement tendance à vérifier la possibilité des opérations avant de les exécuter ; plutôt que de voir survenir un souci et devoir le traiter (ou ne pas le traiter et laisser le code se crasher lamentablement).
Cependant, il existe des cas où cela n'est pas possible ou trop difficile. Celui qui veut par exemple ouvrir un fichier devra d’abord vérifier que le fichier existe, puis qu'il est du bon type, puis qu'il est accessible, etc. Et malgré tous les tests qu'il fera, il y aura toujours un risque potentiel que le fichier change d’état durant le laps de temps qu'il y aura eu entre les tests et l’ouverture réelle (OS multi-utilisateurs).
Devant les difficultés sous‑jacentes de cette démarche, le programmeur Python habitué préfèrera s'appuyer sur les mécanismes de protection intégrés au langage et lancer directement l’opération en présumant qu'elle fonctionnera ; tout en se protégeant des échecs éventuels en récupérant les exceptions associées pour les gérer. Bref, en Python, il vaut mieux demander « pardon » que « permission ».