UKOnline

Gestion d'erreurs

On a déjà pu voir de nombreuses situations où l'exécution d'un programme provoque une erreur. Lorsque c'est le cas, l'exécution s'arrête immédiatement et l'interpréteur Python affiche une trace d'erreur.

Cette dernière fournit des informations quant au chemin d'exécution qui a mené jusqu'à l'erreur et sur la cause de cette dernière. Prenons, par exemple, le programme suivant :

Si on l'exécute, on obtient le résultat suivant :

Alexis a obtenu 90.0 %
Traceback (most recent call last):
  File "program.py", line 5, in <module>
    print('Sébastien a obtenu', percentage(6, 0), '%')
  File "program.py", line 2, in percentage
    return score / total * 100
ZeroDivisionError: division by zero

On voit donc, sur la première ligne, qu'Alexis a obtenu $90$%. On ne voit par contre pas la note de Sébastien, mais un message d'erreur à la place. En décortiquant cette trace d'erreur, on peut y identifier plusieurs informations par rapport à l'erreur qui s'est produite :

  • La première ligne, qui commence par Traceback, est le début de la trace d'erreur. Après cette ligne, on retrouve l'ordre des appels qui ont causé l'erreur.
  • Le premier élément de la trace d'erreur indique son origine. Dans ce cas-ci, l'erreur provient de l'exécution de l'instruction de la ligne~5 du fichier program.py :
    File "program.py", line 5, in <module>
    	    print('Sébastien a obtenu', percentage(6, 0), '%')
  • Comme l'erreur est apparue suite à un appel de fonction, la trace d'erreur fournit plus d'informations quant à l'instruction précise dans la fonction qui a provoqué l'erreur. Dans ce cas-ci, l'erreur provient de l'exécution de l'instruction de la ligne~2, dans la fonction percentage :
    File "program.py", line 2, in percentage
    	    return score / total * 100
  • Enfin, la dernière ligne de la trace d'erreur explique ce qui a provoqué l'erreur. Dans ce cas-ci, il s'agit d'une division par zéro :
    ZeroDivisionError: division by zero

La trace d'erreur permet donc de retracer l'erreur depuis son origine jusqu'à l'instruction précise qui l'a provoquée. Elle essaie également de fournir une explication utile quant à sa cause.

Comment gérer cette erreur ? Une première manière de faire consiste à rendre la fonction percentage la plus robuste possible, c'est-à-dire qu'elle ne doit jamais produire d'erreur. Pour cet exemple, on pourrait donc s'assurer qu'elle se comporte bien, peu importe la valeur de total, en renvoyant une valeur spéciale lorsque total vaut $0$ :

Cette solution n'est pas encore idéale car il faudrait vérifier que les deux paramètres soient des nombres positifs et que score soit inférieur à total. On continue avec cet exemple en présentant d'autres solutions pour gérer ses erreurs, plus loin dans ce chapitre.

Type d'erreur

On peut distinguer trois types d'erreurs :

  • Une erreur de syntaxe survient lorsque le code source du programme est mal formé. Une telle erreur se produit, par exemple, lorsqu'on oublie la condition d'un if, lorsqu'on a un else sans if associé, lorsqu'on a mal écrit un mot réservé, etc.
  • Une erreur d'exécution survient lorsqu'un programme, syntaxiquement correct, effectue une opération interdite. Une telle erreur se produit, par exemple, lorsqu'on tente de faire une division par zéro, lorsqu'on ajoute une liste dans un ensemble, lorsqu'on tente d'accéder à une variable d'instance privée hors de la classe, etc.
  • Une erreur logique survient lorsqu'un programme, sans erreur de syntaxe ni d'exécution, ne produit pas le résultat correct attendu. Par exemple, si on a écrit length + width au lieu de length * width pour calculer la surface d'un rectangle, le programme se terminera sans erreur, mais pas avec la bonne réponse.

Erreur de syntaxe

Lorsqu'un programme Python comporte une erreur de syntaxe, elle ne sera détectée que lors de l'exécution, étant donné qu'il s'agit d'un langage interprété et que l'interpréteur ne passe pas en revue tout le code source avant exécution. Il se peut donc très bien qu'un programme comportant des erreurs de syntaxe se soit toujours exécuté sans erreurs. C'est, par exemple, le cas lorsqu'on a un if-else dont le bloc else n'a jamais été exécuté, alors qu'il comporte une erreur de syntaxe. Examinons le code d'exemple suivant, qui comporte une erreur de syntaxe :

Si on tente d'exécuter ce programme, on se retrouve face à l'erreur suivante :

  File "program.py", line 2
    if score > 10
                ^
SyntaxError: invalid syntax

On voit directement qu'il s'agit d'une erreur de syntaxe grâce à la dernière ligne qui commence par SyntaxError. L'interpréteur tente de décrire et situer l'erreur le plus précisément possible. Il indique qu'il y a un souci à la ligne 2 et plus précisément tout à la fin de l'instruction comme pointé par le caractère ^ de la troisième ligne. On se rend en fait compte qu'il manque le : après la condition du if.

Il faut aussi savoir qu'il y a deux cas particuliers d'erreurs de syntaxe pour lesquels le type d'erreur sera différent :

  • L'indentation du code n'est pas correcte (IndentationError).
  • Il y a une inconsistence entre l'utilisation d'espaces et de tabulations pour l'indentation d'un même bloc (TabError).

Erreur d'exécution

Une erreur d'exécution se produit lorsqu'une opération interdite a été effectuée. Voici plusieurs situations qui peuvent se produire, étant donné ce qu'on a déjà vu :

  • Une opération arithmétique ne peut pas être effectuée : division par zéro (ZeroDivisionError), racine carrée d'un nombre négatif (ValueError).
  • Un opérateur ou une fonction est utilisé avec une donnée ou variable du mauvais type (TypeError).
  • Un package n'a pas su être importé (ImportError).
  • Une variable ou une fonction avec le nom précisé n'a pas su être trouvé (NameError).
  • Un accès à un élément d'une séquence ne peut pas être effectué : mauvais indice dans une liste (IndexError), clé inexistante dans un dictionnaire (KeyError).
  • Le nombre maximal d'appels récursifs a été atteint (RecursionError).

L'exemple suivant provoque une erreur d'exécution, car on dépasse des bornes d'une liste dont on souhaite afficher les éléments :

On commence bien avec $0$ comme premier indice, mais on va trop loin pendant la boucle puisqu'on s'arrête à la taille de la liste, alors que le plus grand indice vaut un de moins que cette dernière. Les trois valeurs de la liste sont donc bien affichées, mais s'ensuit une erreur d'exécution :

1
2
3
Traceback (most recent call last):
  File "program.py", line 5, in <module>
    print(data[i])
IndexError: list index out of range

Erreur logique

Enfin, le dernier type d'erreur est le plus difficile à déceler. Lorsqu'un programme comporte une erreur logique, il s'exécute en effet sans erreur, si ce n'est que le résultat produit n'est pas celui attendu. Supposons, par exemple, que l'on écrive une fonction dont le but est de calculer le périmètre d'un rectangle :

On n'a malheureusement pas été attentif à la priorité des opérateurs et on a bêtement écrit la formule « longueur plus largeur fois deux » comme appris en primaires. Le problème est que le calcul fait est $longueur + (largeur \times 2)$; l'exécution du programme affiche donc $4$ au lieu de $6$. Pour s'en rendre compte, il faudrait pouvoir dire à Python le résultat attendu. Mais d'un autre côté, si on écrit un programme c'est aussi pour que Python calcule ce résultat à notre place !

Heureusement, il existe des techniques, dont une basée sur des tests unitaires, qui permettent de traquer ce type d'erreur. Ces dernières ne sont pas abordées dans ce livre introductif, mais font l'objet d'autres ouvrages plus avancés.

Documentation

Une première façon de gérer les erreurs passe par la documentation. On a déjà abordé rapidement ce sujet au chapitre 4, mais revenons maintenant plus en détails dessus. On va se concentrer uniquement sur la documentation des fonctions.

Le but de la documentation est de permettre à l'utilisateur d'une fonction de savoir comment l'appeler correctement et comment interpréter sa valeur de retour. Elle donne des conditions à respecter sur les valeurs des paramètres et explique quelles seront les différentes valeurs de retour possibles. Voici ce que ça pourrait donner pour la fonction percentage vue au début de ce chapitre :

Ce qu'on vient d'écrire est une documentation informelle. Il n'y a pas vraiment de règles à suivre et on peut se contenter d'un texte en langue naturelle. L'important est de n'oublier aucune information, afin que l'on puisse appeler et utiliser la fonction, et interpréter sa valeur de retour, sans ambigüité.

L'autre façon de décrire proprement une fonction consiste à établir ses spécifications comme présenté au chapitre 4. Pour rappel, les deux éléments suivants sont à définir :

  • les préconditions sont les conditions qui doivent être satisfaites sur les paramètres et l'état global du programme, avant l'appel de la fonction.
  • les postconditions sont les conditions qui seront satisfaites sur la valeur de retour et sur l'état global du programme, après l'appel de la fonction, si les préconditions ont été respectées.

Une spécification est donc un contrat entre celui qui implémente une fonction et celui qui l'utilise. L'utilisateur s'engage à respecter les préconditions avant d'appeler la fonction et le programmeur lui garantit que les postconditions seront satisfaites en retour. Voici une nouvelle version de la fonction percentage, avec sa spécification :

Étant donné qu'il y a un contrat, le programmeur ne doit plus se soucier du cas où les paramètres sont négatifs, ou de celui où total est nul. Il ne doit garantir le bon comportement de la fonction que pour les cas où les préconditions sont respectées, ce qui simplifie la fonction.

Génération de la documentation

Comme on l'a déjà vu au chapitre 4, il est de bon usage, en Python, d'insérer la documentation d'une fonction sous forme d'un Docstring. Il s'agit simplement de placer une chaine de caractères, souvent délimitée par des guillemets triples pour pouvoir l'écrire sur plusieurs lignes, comme première instruction du corps de la fonction. L'exemple précédent se réécrit donc comme suit :

Si on génère la documentation de cette fonction, à l'aide de l'outil pydoc, on obtient le résultat présenté à la figure 1, qui reprend la documentation de toutes les fonctions présentes dans un module. L'outil pydoc permet également d'exporter cette documentation en HTML.

Documentation avec DocString
La documentation insérée sous la forme de Docstring peut être visualisée à l'aide de l'outil pydoc.