UKOnline

Mécanisme d'exception

On a vu que l'on peut donc gérer les erreurs à l'aide de l'instruction if-else et en prévoyant des valeurs de retour spéciales. Cette technique n'est malheureusement pas toujours utilisable, notamment lorsque la fonction définie ne renvoie rien.

Voyons maintenant le mécanisme d'exception, présent dans les langages de programmation orienté objet, qui permet de gérer des exécutions exceptionnelles qui ne se produisent qu'en cas d'erreur.

Instruction try-except

La construction de base à utiliser est l'instruction try-except qui se compose de deux blocs de code. On place le code « risqué » dans le bloc try et le code à exécuter en cas d'erreur dans le bloc except. Partons d'un exemple pour comprendre son utilisation :

On demande donc à l'utilisateur son année de naissance, grâce à la fonction prédéfinie input qui renvoie, pour rappel, une chaine de caractères. On souhaite ensuite calculer son âge en soustrayant son année de naissance à $2016$. Pour cela, il faut convertir la valeur de la variable birthyear en un int. Cette conversion peut échouer si la chaine de caractères entrée par l'utilisateur n'est pas un nombre. Voyons deux scénarios possibles d'exécution :

  • Si l'utilisateur entre un nombre entier, l'exécution se passe sans erreur et son âge est calculé et affiché :
    Année de naissance ? 1994
    Tu as 22 ans.
    Fin du programme.
  • Si l'utilisateur entre une chaine de caractères quelconque, qui ne représente pas un nombre entier, un message d'erreur est affiché :
    Année de naissance ? deux
    Erreur, veuillez entrer un nombre.
    Fin du programme.

Dans le premier cas, la conversion s'est passée sans soucis, et le bloc try a donc pu s'exécuter intégralement sans erreur. L'exécution du programme se poursuit donc après l'instruction try-except. Dans le second cas, une erreur se produit dans le bloc try, lors de la conversion. L'exécution de ce bloc s'arrête donc immédiatement et passe au bloc except, avant de continuer également après l'instruction try-except.

On place donc uniquement le code qui est susceptible de générer une erreur dans le bloc try, avec le code qui en dépend. Il est important de ne pas placer trop de code dans ce dernier. Améliorons l'exemple précédent pour demander à l'utilisateur son année de naissance, en boucle jusqu'à ce qu'il fournisse une valeur correcte :

On initialise une variable valid à la valeur False. On rentre ensuite dans une boucle while qui va se répéter tant que la variable valid ne passe pas à True. Le corps de la boucle commence par demander à l'utilisateur son année de naissance, à l'aide de la fonction prédéfinie input. On entre ensuite dans une zone critique, placée donc dans un bloc try. On commence par tenter de convertir la variable birthyear en un entier de type int. Si la conversion réussi, on s'assure que la valeur est bien comprise entre $0$ et $2016$ et, dans ce cas, on passe la valeur de valid à True, sinon on affiche un message d'erreur. Dans le bloc except, on affiche également un message d'erreur. On finit par calculer l'âge et l'afficher, en dehors de la boucle while.

Voici le résultat d'une exécution du programme :

Année de naissance ? BLA
Veuillez entrer un nombre naturel.

Année de naissance ? -12
L'année doit être comprise entre 0 et 2016.

Année de naissance ? 1973
Tu as 43 ans.

La boucle s'est donc exécutée trois fois avant que l'utilisateur n'entre une valeur valide et obtienne ainsi son âge.

Le type Exception

Comme on l'a vu en début de chapitre, différents types d'erreurs peuvent survenir. Lorsqu'on utilise l'instruction try-except, le bloc except capture toutes les erreurs possibles qui peuvent survenir dans le bloc try correspondant. Une exception est en fait représentée par un objet, instance de la classe Exception. On peut récupérer cet objet en précisant un nom de variable après except comme dans cet exemple :

On récupère donc l'objet de type Exception dans la variable e. Dans le bloc except, on affiche son type et sa valeur. Voici deux exemples d'exécution qui révèlent deux types d'erreurs différents :

  • Si on ne fournit pas un nombre entier, il ne pourra être converti en int et une erreur de type ValueError se produit :
    a ? trois
    <class 'ValueError'>
    invalid literal for int() with base 10: 'trois'
  • Si on fournit une valeur de $0$ pour b, on aura une division par zéro qui produit une erreur de type ZeroDivisionError :
    a ? 5
    b ? 0
    <class 'ZeroDivisionError'>
    division by zero

Capturer une erreur spécifique

Chaque type d'erreur est donc défini par une classe spécifique. On va pouvoir associer plusieurs blocs except à un même bloc try, pour exécuter un code différent en fonction de l'erreur capturée. Lorsqu'une erreur se produit, les blocs except sont parcourus l'un après l'autre, du premier au dernier, jusqu'à en trouver un qui corresponde à l'erreur capturée. Réécrivons l'exemple précédent en capturant les exceptions spécifiques pour les deux cas d'erreur qu'on a pu observer (erreur de conversion et division par zéro) :

Remarquez tout d'abord qu'on n'est pas obligé de spécifier une variable lorsqu'on capture une exception spécifique. Cette fois-ci, lorsqu'une erreur se produit dans le bloc try, ce sera l'un des blocs except seulement qui sera exécuté, selon le type de l'erreur qui s'est produite. Le dernier bloc except est là pour prendre toutes les autres erreurs.

L'ordre des blocs except est très important et il faut les classer du plus spécifique au plus général, celui par défaut devant venir en dernier. En effet, si on commence par un bloc except pour une exception de type Exception, il sera toujours exécuté et tous les autres qui le suivent ne le seront jamais. Dans l'exemple suivant, ce sera donc toujours « Autre erreur. » qui sera affiché dès qu'une erreur se produit dans le bloc try :

On aura, par exemple, le résultat suivant si on spécifie une valeur nulle pour le dénominateur :

a ? 2
b ? 0
Autre erreur.

Sans s'attarder sur la raison précise de ce comportement, qui nécessiterait des notions avancées en programmation orientée objet, il faut savoir que le type Exception englobe les types ValueError et ZeroDivisionError.

Gestionnaire d'erreur partagé

Enfin, il est possible d'exécuter le même code pour différents types d'erreur, en les listant dans un tuple après le mot réservé except. Si on souhaite exécuter le même code pour une erreur de conversion et de division par zéro, il faudrait écrire :

Le deuxième bloc except capture donc les erreurs de type ValueError et ZeroDivisionError. L'exception capturée est stockée dans la variable e que l'on affiche pour avoir des informations sur la cause de l'erreur apparue. On pourrait, par exemple, observer le résultat suivant lors d'une exécution du programme :

a ? 2
b ? 0
Erreur de calcul : division by zero

Propagation d'erreur

Que se passe-t-il lorsqu'on ne capture pas une exception à l'aide d'une instruction try-except ? Cette dernière va en fait remonter la séquence des appels de fonctions. Prenons, par exemple, le programme suivant qui comporte deux fonctions, appelées en chaine :

Le programme appelle donc la fonction compute qui, elle-même, appelle la fonction fun, cette dernière produisant une erreur à cause de la division par zéro. Voici le résultat que l'on obtient en exécutant ce programme :

Traceback (most recent call last):
  File "program.py", line 7, in <module>
    compute()
  File "program.py", line 5, in compute
    fun()
  File "program.py", line 2, in fun
    print(1 / 0)
ZeroDivisionError: division by zero

Dans un sens, la trace d'erreur permet de suivre le déroulement de l'exécution, partant du programme principal vers la fonction fun, en passant par la fonction compute. Dans l'autre sens, on peut suivre la propagation de l'erreur depuis la fonction fun vers le programme principal, en passant par la fonction compute.

Une erreur qui n'est pas capturée va donc se propager, en remontant la séquence des appels de fonctions qui ont été faits. On peut modifier le programme en capturant, par exemple, l'erreur dans la fonction compute :

Dans ce cas, l'erreur qui est générée dans la fonction fun va juste se propager dans la fonction compute où elle sera arrêtée par l'instruction try-except. Le résultat de l'exécution est donc :

Erreur.

Il est important de capturer l'erreur à l'endroit où son traitement est le plus approprié, mais on ne va pas entrer dans les détails dans le cadre de ce livre consacré aux bases.

Le bloc finally

Parfois, on souhaite exécuter des instructions dans tous les cas, avant que l'exécution de continue après le bloc try-except. Le problème est que lorsqu'une erreur se produit dans le bloc try, l'exécution de ce dernier est interrompue pour se poursuivre dans le bloc except correspondant.

Pour cela, on peut utiliser le mot réservé finally qui permet d'introduire un bloc qui sera exécuté soit après que le bloc try se soit exécuté complètement sans erreur, soit après avoir exécuté le bloc except correspondant à l'erreur qui s'est produite lors de l'exécution du bloc try. On obtient ainsi une instruction try-except-finally dont voici un exemple d'utilisation :

Si l'utilisateur fournit des valeurs correctes pour a et b, l'exécution du bloc try se passera sans erreur, c'est-à-dire que le résultat de la division de a par b sera affiché, puis que la phrase signalant que la mémoire est nettoyée sera affichée :

Début du calcul.
a ? 2
b ? 8
Résultat : 0.25
Nettoyage de la mémoire.
Fin du calcul.

Le bloc finally a donc bien été exécuté, après l'exécution sans erreur du bloc try et avant que l'exécution du programme ne continue après l'instruction try-except.

Par contre, si une erreur se produit, alors le résultat ne sera pas affiché puisque le bloc try sera arrêté ; on aura simplement l'affichage d'un message d'erreur. Par contre, la phrase signalant que la mémoire est nettoyée sera de nouveau affichée témoignant bien du fait que le bloc finally a été exécuté, cette fois-ci après le bloc except, mais toujours avant la reprise de l'exécution après l'instruction try-except :

Début du calcul.
a ? 2
b ? 0
Erreur.
Nettoyage de la mémoire.
Fin du calcul.

Enfin, sachez que l'on peut se limiter à une instruction try-finally, sans définir un seul bloc except. Dans ce cas, si une erreur se produit dans le bloc try, elle sera propagée après exécution du bloc finally.