Gestion des erreurs : les exceptions
Retour au sommaire
Introduction
- SourceUn programme exécuté est susceptible de commettre des erreurs parce qu'il a été mal programmé (ce qu'on appelle des bugs), mais également d'utiliser des composants informatiques qui ne dépendent pas de lui (le disque dur, le réseau) ou des entrées de l'utilisateur (des données) qui peuvent se comporter de façon inappropriée. Par exemple, l'utilisateur vous demande d'ouvrir un fichier qui n'existe pas.
Dans ces conditions, il est impossible de continuer l'exécution du programme correctement : il faut réagir, en redemandant à l'utilisateur d'entrer un nom correct, ou bien en interrompant les calculs. Python propose un mécanisme de gestion des erreurs qui permet de séparer le code correspondant à la logique de votre programme de celui qui traite les situations d'erreurs. C'est de ce mécanisme dont nous allons maintenant parler.
Les exceptions
- SourceLa séparation que propose Python considère que les erreurs n'interviennent pas dans la majorité des cas. En fait, on peut considérer que la plupart du temps, les choses se passent bien (sinon il faut peut-être reconsidérer l'organisation de votre programme). Mais à cause de raisons extérieures (mauvaise utilisation du code, environnement extérieur incohérent), certains calculs peuvent être impossibles à réaliser (par exemple, vous ne pouvez pas lire dans un fichier parce qu'il n'existe pas, ou bien la connexion avec un serveur distant a été coupée brutalement).
Il est alors nécessaire de savoir interrompre les calculs, de propager ces erreurs et de pouvoir les traiter : si elles sont récupérables, on essaye de remettre le programme dans le droit chemin, sinon on essaye de limiter les dégâts et on signale à l'utilisateur un problème.
Pour ce faire, Python propose d'utiliser ce qu'on appelle des exceptions. Ce sont des interruptions de calcul qui permettent de faire ce dont nous venons de parler. Lorsqu'une erreur survient, on dit que Python lève ou déclenche une exception, qui fait sortir le programme de son déroulement normal (les instructions qui suivent peuvent ne pas être exécutées). Un petit exemple :
print("Ce programme divise 7 par le nombre entré") encore = "o" while encore == "o": a = input() print(7/int(a)) print("Encore ? [o/n]") encore = input() print("Fin du programme")
Il n'y a normalement rien d'effrayant pour vous dans ce programme si
vous avez lu les chapitres précédents. Enregistrez-le et testez-le un
petit peu : que se passe-t-il si vous entrez 0 pour a ?
Naturellement, vous savez qu'on ne peut pas diviser par zéro. Mais
notre programme ne tient pas compte de cette éventualité : si
l'utilisateur fait ce choix, le programme est interrompu brutalement
(la dernière ligne, par exemple, n'est pas exécutée) et Python affiche
alors une erreur dans le genre de
print(7/int(a)) ZeroDivisionError: int division or modulo by zero
Ceci est une exception. Elle a été déclenchée par une instruction
particulière (la ligne de code donnée par Python), possède un nom
(ZeroDivisionError), et est accompagnée d'un message explicatif. Que
se passe-t-il maintenant si, plutôt que de rentrer 0, nous rentrons
une chaîne qui ne peut être convertie en nombre ? Essayons par exemple
la chaîne salut :
print(7/int(a)) ValueError: invalid literal for int() with base 10: 'salut'
Une autre exception est levée, différente de la première. Mais le résultat est le même : les calculs s'arrêtent là, l'utilisateur est averti.
Prévenir une exception
- SourceSi nous n'avion pas eu les exceptions...
Notez que, dans les deux cas précédents, nous aurions pu gérer
l'erreur d'une façon ou d'une autre : dans le premier cas, un simple
test pour vérifier que l'utilisateur n'a pas rentré 0 suffit. Dans le
second, on aurait pu utiliser la méthode isdigit de la chaîne a
pour vérifier qu'on pouvait bien la convertir en entier.
Mais de nombreuses autres exceptions peuvent être rencontrées dans des situations à peine plus complexes. Même si la plupart peuvent être évitées avec des tests supplémentaires, il faut bien comprendre que les exceptions proposent un mécanisme plus élégant (séparation du code "normal" et de la partie de traitement des erreurs, nous allons y revenir), plus puissant (propagation des erreurs, ce qui permet de laisser le choix à un autre développeur de les gérer à sa façon) et plus universel (toutes les exceptions ont la même forme, alors que dans l'exemple précédent les deux erreurs ne se traitent pas de la même façon).
De nouvelles constructions
Comment ferons-nous alors pour récupérer d'une erreur exceptionnelle,
ce qui est nécessaire pour construire des programmes suffisamment
stables pour l'utilisateur final ? Python met à notre disposition une
construction particulière, à l'aide de nouveaux mot-clefs et de
nouveaux blocs : try et except (ainsi que des variantes moins
importantes dans un premier temps).
Le code susceptible de générer une exception est alors placé dans le
bloc try, et le code de traitement dans le bloc except suivi de
l'erreur traitée. Revoici le coeur de notre programme précédent :
while encore == "o": a = input() try: print(7/int(a)) except ZeroDivisionError: print("Impossible de diviser par zéro...") except ValueError: print("Merci d'entrer un nombre correct en base 10") print("Encore ? [o/n]") encore = input()
Comme vous pouvez le voir, nous gérons ici les deux erreurs
précédemment rencontrées. Cependant, on pourrait, quand l'utilisateur
provoque une erreur, ne pas vouloir réafficher le message "Encore ?
[o/n]" et boucler directement. Autrement dit, on voudrait que les deux
dernières lignes ne soient exécutées que si tout s'est bien déroulé
précédemment. Pour cela, Python propose un bloc else pour la
construction try, dont le contenu n'est pas exécuté si une exception
a été rencontrée. On aurait alors :
while encore == "o": a = input() try: print(7/int(a)) except ZeroDivisionError: print("Impossible de diviser par zéro...") except ValueError: print("Merci d'entrer un nombre correct en base 10") else: print("Encore ? [o/n]") encore = input()
Récupérer d'une erreur sérieuse
L'exemple précédent est assez trivial (il consiste surtout en des
messages affichés à l'utilisateur via print), mais d'autres
situations plus graves peuvent survenir. Dans quelques chapitres, nous
apprendrons ainsi à nous servir des fichiers, puis du réseau, et puis
un jour sûrement vous utiliserez des bases de données : bref, des
contextes dans lesquels il vaut mieux manipuler prudemment les
différents éléments du programme.
Si une exception survient par exemple alors que vous communiquez sur le réseau, interrompant vos opérations, il sera probablement nécessaire de "nettoyer" un petit peu votre environnement. Si des connexions restent ouvertes, il faudra les fermer proprement ; si diverses ressources ont été mobilisées par votre programme et ne sont plus nécessaires, alors exception ou pas exception il sera important de les libérer.
Ceci se fait à l'aide d'un nouveau mot clef : finally, qui se place
après tous les blocs précédemment évoqués. Comme else il est
optionnel, mais il faut savoir l'utiliser. Le bloc finally est
toujours exécuté : qu'il y ait une exception ou pas, un bloc else
ou pas, et même si le programme se termine, il est exécuté. Bien sûr,
en cas de boucle infinie dans un morceau de code précédent, ou d'une
coupure de courant, il ne sera pas atteint et donc pas exécuté,
mais dans des circonstances moins extra-ordinaires il échappe au flux
d'exécution. Voici une petite démonstration :
def f(): try: 1/0 # Une source de problèmes. except ZeroDivisionError: return # En temps normal, la fonction retourne directement... finally: print("Ce message est quand même affiché !") f()
Notez bien que le code du bloc finally n'est pas équivalent à un
code placé en dessous d'une construction try/except : dans le cas
précédent, si l'appel à print n'avait pas été placé dans un tel
bloc, il n'aurait pas été exécuté (à cause du return). Nous
reverrons finally dans un futur proche.
Attrapez-les toutes
Lorsque votre code dépend d'autres modules qui ne sont pas les vôtres,
vous ne pouvez pas nécessairement prévoir toutes les exceptions
susceptibles d'être levées. De plus, il est courant de vouloir
regrouper des traitements similaires dans un même bloc except. La
syntaxe de cette construction est donc un peu plus souple que ce que
nous avons décrit : premièrement, il est possible de regrouper des
exceptions dans un même bloc en les mettant entre parenthèses et en
les séparant par des virgules, sous cette forme :
try: ... except (Exception1, Exception2...): ...
On dit que l'on forme un tuple, c'est une séquence particulière que nous reverrons prochainement. Il est également possible de ne pas préciser quelle exception on cherche à attraper - ce qui nous permet de toutes les traiter dans un même bloc. Pour reprendre notre exemple,
while encore == "o": a = input() try: print(7/int(a)) except ZeroDivisionError: print("Impossible de diviser par zéro...") except ValueError: print("Merci d'entrer un nombre correct en base 10") except: print("Une exception inconnue a eu lieu...") else: print("Encore ? [o/n]") encore = input()
Néanmoins, il est dangereux de procéder ainsi trop souvent : les exceptions ne sont pas nécessairement un problème, elles sont également une source d'information sur le fonctionnement de vos programmes. En les récupérant ainsi, vous risquez de masquer des problèmes auxquels vous n'aviez pas pensé.
Lever une exception
- SourceLe mot clef raise
Python nous permet également de lever nos propres exceptions, à l'aide
du mot-clef raise. Ceci est par exemple très utile lorsque nous
écrivons des modules susceptibles d'être utilisés par d'autres
personnes. Si certaines conditions doivent être respectées et qu'elles
ne le sont pas, on peut très simplement déclencher une exception en
écrivant raise Exception(raison).
Prenons par exemple le cas d'une fonction censée calculer la moyenne arithmétique des valeurs contenues par une liste (somme des valeurs divisée par la taille de la liste) :
def moyenne(liste): return sum(liste)/len(liste)
Que se passe-t-il si la liste vide est passée en argument ? Python reconstitue la liste des appels de fonctions qui ont conduit à l'exception, et nous donne une erreur de la forme
Traceback (most recent call last):
File "<pyshell#20>", line 1, in <module>
moyenne([])
File "/Users/poulet/Python/Cours/6_3.py", line 2, in moyenne
return sum(liste)/len(liste)
ZeroDivisionError: int division or modulo by zero
Ici l'erreur se comprend assez bien, mais on pourrait avoir envie de
la rendre plus explicite (parce que c'est mieux). Il faudrait donc
préciser à l'utilisateur que la fonction moyenne ne peut pas
travailler sur la liste vide, c'est une valeur incorrecte
d'argument pour cette fonction. Nous allons déclencher une exception
de type ValueError. Voici notre fonction améliorée :
def moyenne(liste): if not liste: # Si la liste est vide raise ValueError("Une liste non-vide est attendue") return sum(liste)/len(liste)
Nous verrons dans quelques chapitres comment définir nos propres types d'exceptions.
Redéclencher une exception
Il est également possible, quand on ne sait pas vraiment quoi faire
pour traiter une exception ou que le programme doit de toute façon
être interrompu, d'effectuer quelques opérations dans un bloc except
et de redéclencher l'exception rencontrée à l'aide du mot clef raise
employé seul :
try: ... except: print("Une erreur a été rencontrée...") raise
Dans ce cas là, quelle que soit l'exception rencontrée et attrapée par
except, elle sera redéclenchée. Dans une telle situation,
l'utilisation de except sans exception est acceptable, car on
n'empêche pas l'erreur de se propager.
Notez que le mot clef raise employé seul sans bloc except provoque
une exception de type RuntimeError, nous avertissant qu'il n'y a pas
d'exception à redéclencher. C'est assez ironique non ?
Les assertions
Enfin, il est parfois pratique de déclencher facilement une exception
à partir d'une expression booléenne. Si celle-ci est évaluée à True,
tout va bien, et si elle est évaluée à False, on provoque l'erreur.
Ceci se fait à l'aide du mot clef assert condition. Revoici notre
fonction moyenne :
def moyenne(liste): assert liste return sum(liste)/len(liste)
Si jamais liste est évaluée comme False (donc en particulier si
c'est la liste vide), assert déclenche une exception de type
AssertionError. Cela donne peu d'informations, donc privilégiez ce
genre de rapport d'erreur aux programmes encore en développement (cela
permet de tester rapidement qu'une certaine condition est vérifiée).