Gestion des erreurs

On a vu jusqu’à présent différentes situations où une opération provoquait le plantage de python et interrompait l’exécution.

>>> x = 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> ls = [1, 2]
>>> ls[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

ATTENTION ceci n’est pas tellement gênant dans une session interactive. Mais lors de l’exécution d’un script prenant plusieurs heures, cela entraine la perte de toutes les données non sauvegardées sur disque!

REMARQUE le type d’erreur générée est différent dans les trois cas ci-dessus.

On va s’intéresser dans cette leçon à la façon d’intercepter ces erreurs (plus précisément ces exceptions) afin de pouvoir continuer l’exécution. Il faudra utiliser ce mécanisme avant d’effectuer une opération

On montrera ensuite comment générer soi-même des exceptions.

Interception

On va intercepter les exceptions en utilisant l’instruction composée try.
Commençons pas montrer l’utilisation sur les exemples introduits ci-dessus.

>>> try:
...     x = 1 / 0
... except ZeroDivisionError:
...     print("Diviser par zéro c'est mal!")
...     x = float("NaN")
...
Diviser par zéro c'est mal!
>>> x
nan
>>> try:
...     y
... except NameError:
...     print("Pas de variable y!")
...
Pas de variable y!
>>> ls = [1, 2]
>>> try:
...     ls[3]
... except IndexError:
...     print("Pas d'élément d'indice 3")
...
Pas d'élément d'indice 3

La syntaxe générale de cette instruction est la suivante:

try:
    INSTRUCTION_1
    ...
    INSTRUCTION_N
except Erreur_1:
    INSTRUCTION_1_1
    ...
    INSTRUCTION_1_N1
except Erreur_2:
    INSTRUCTION_2_1
    ...
    INSTRUCTION_3_N2
...
except Erreur_M:
    INSTRUCTION_M_1
    ...
    INSTRUCTION_M_NM
else:
    INSTRUCTION_E_1
    ...
    INSTRUCTION_E_P
finally:
    INSTRUCTION_F_1
    ...
    INSTRUCTION_F_Q

REMARQUE il faut au moins un bloc except mais les blocs else et finally sont optionnels.

>>> try:
...     1 / 0
...
  File "<stdin>", line 3

    ^
SyntaxError: invalid syntax

REMARQUE la dernière instruction except, peut, ne pas avoir d’erreur associée, on intercepte alors toutes les erreurs.

>>> try:
...     1 / 0
... except:
...     print("Problème")
...
Problème

REMARQUE les instructions du corps principal de try sont exécutées jusqu’à l’instruction problématique.

>>> try:
...     print("Au dessus on exécute")
...     x = 1 / 0
...     print("Pas au dessous.")
... except ZeroDivisionError:
...     print("Mais on passe dans la branche except!")
...
Au dessus on exécute
Mais on passe dans la branche except!

REMARQUE on essaye généralement de réduire au maximum le nombre d’instructions dans le bloc try (idéalement une seule instruction). Ceci afin de ne pas induire en erreur un lecteur quant à l’instruction responsable du problème (ou au moins de ne pas lui compliquer la vie).

Pour avoir la possibilité d’inclure des instructions qui ne sont exécutées que si tout se passe bien on utilise le bloc else.

>>> try:
...     x = 1 / 0
... except ZeroDivisionError:
...     print("PROBLEME!")
... else:
...     print("OK")
...
PROBLEME!
>>> try:
...     x = 1 / 2
... except ZeroDivisionError:
...     print("PROBLEME!")
... else:
...     print("OK")
...
OK

REMARQUE le bloc finally sera exécuté quoi qu’il se passe. C’est à dire même si une exception n’a pas été interceptée.

>>> try:
...     x = 1 / 0
... except IndexError:
...     print("Interceptée!")
... finally:
...     print("FINALEMENT")
...
FINALEMENT
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

Typiquement avant que la syntaxe with soit introduite, l’ouverture d’un fichier était dans un bloc try et la fermeture dans finally.

REMARQUE on peut aussi récupérer le message précis d’erreur au moment de l’interception en utilisant as.

>>> ls = list()
>>> try:
...     ls[0]
... except IndexError as e:
...     print(e)
...
list index out of range

REMARQUE les exceptions sont des objets à part entières qui sont cachés dans le module __builtins__ de l’espace de nom global.

>>> from rich import print
>>> from rich.columns import Columns
>>> print(Columns(dir(__builtins__)))
ArithmeticError      AssertionError         AttributeError     BaseException
BlockingIOError      BrokenPipeError        BufferError        BytesWarning
ChildProcessError    ConnectionAbortedError ConnectionError    ConnectionRefusedError
ConnectionResetError DeprecationWarning     EOFError           Ellipsis
EnvironmentError     Exception              False              FileExistsError
FileNotFoundError    FloatingPointError     FutureWarning      GeneratorExit
IOError              ImportError            ImportWarning      IndentationError
IndexError           InterruptedError       IsADirectoryError  KeyError
KeyboardInterrupt    LookupError            MemoryError        ModuleNotFoundError
NameError            None                   NotADirectoryError NotImplemented
NotImplementedError  OSError                OverflowError      PendingDeprecationWarning
PermissionError      ProcessLookupError     RecursionError     ReferenceError
ResourceWarning      RuntimeError           RuntimeWarning     StopAsyncIteration
StopIteration        SyntaxError            SyntaxWarning      SystemError
SystemExit           TabError               TimeoutError       True
TypeError            UnboundLocalError      UnicodeDecodeError UnicodeEncodeError
UnicodeError         UnicodeTranslateError  UnicodeWarning     UserWarning
ValueError           Warning                ZeroDivisionError  __build_class__
__debug__            __doc__                __import__         __loader__
__name__             __package__            __spec__           abs
all                  any                    ascii              bin
bool                 breakpoint             bytearray          bytes
callable             chr                    classmethod        compile
complex              copyright              credits            delattr
dict                 dir                    divmod             enumerate
eval                 exec                   exit               filter
float                format                 frozenset          getattr
globals              hasattr                hash               help
hex                  id                     input              int
isinstance           issubclass             iter               len
license              list                   locals             map
max                  memoryview             min                next
object               oct                    open               ord
pow                  print                  property           quit
range                repr                   reversed           round
set                  setattr                slice              sorted
staticmethod         str                    sum                super
tuple                type                   vars               zip

On utilise ici rich uniquement pour avoir un affichage tabulaire. On regardera la section Pour aller plus loin si on veut des renseignements sur la hiérarchie entre erreurs.

Style

Finissons cette partie sur une remarque de style. On peut opposer deux façons de coder certaines opérations pouvant générer une exception. Pour illustrer ces deux façons supposons qu’on a récupéré un dictionnaire dico et qu’on veut appliquer une fonction ma_fonction et la valeur associée à "clef" sans être certain que celle-ci existe.

  1. On peut vérifier que l’opération est possible avant de l’effectuer, en anglais LBYL (look before you leap)
>>> if "clef" in dico:
        resultat = ma_fonction(dico["clef"])
  1. Alternativement on peut essayer d’effectuer l’opération et se tenir prêt à intercepter l’exception, en anglais EAFP (easier to ask for forgiveness than permission)
>>> try:
        valeur = dict["clef"]
    except KeyError:
        pass
    else:
        resultat = ma_fonction(valeur)

Génération

On va montrer ici très rapidement comment générer soi-même des exceptions. Ceci est effectuée au moyen de l’instruction raise à laquelle on passe un objet Exception avec éventuellement un message d’erreur inclus. Donnons un exemple de fonction:

>>> def divise(a, b):
...     if b == 0:
...             raise ValueError("Pas de division par zéro SVP!")
...     return a / b
...
>>> divise(1, 2)
0.5
>>> divise(1, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divise
ValueError: Pas de division par zéro SVP!

REMARQUE lorsqu’on code des fonctions générant des exceptions on les utilise dans le style EAFP. Alors que dans le style LBYL on se retrouve souvent à coder deux fonctions pour une même tache: - la fonction qui vérifie si la tâche est possible - et la fonction qui effectue réellement la tache

Regarder l’exercice 1. pour plus de détails.

REMARQUE il est possible de créer soi-même ses propres exceptions. Ceci est rarement utile à moins de développer un framework on n’en parlera donc pas ici. raise

Exercices

  1. Coder dans style LBYL et dans le style EAFP la tache consistant à récupérer une valeur entière dans une liste de nombres.

Pour aller plus loin

>>> help("try")
>>> help("raise")
>>> help("EXCEPTIONS")

Corrections

  1. Dans le style LBYL ```python def est_dans_la_liste(valeur: int, nombres: List[int]) -> bool: “”“Test l’appartenance.”“” for nombre in nombres: if valeur == nombre: return True return False

def recupere_indice(valeur: int, nombres: List[int]) -> bool: “”“Renvoie le premier indice ou nombre vaut valeur.”“” for indice, nombre in enumerate(nombres): if nombre == valeur: return indice

if est_dans_la_liste(valeur=VALEUR, nombres=NOMBRES): RESULTAT = recupere_indice(valeur=VALEUR, nombres=NOMBRES) else: print(“PROBLEME!”) ```

Dans le style EAFP

def recupere_indice(valeur: int, nombres: List[int]) -> int:
    """Renvoie l'indice du premier nombre valant valeur."""
    for indice, nombre in enumerate(nombres):
        if valeur == nombre:
            return indice
    raise ValueError("valeur n'est pas un des nombres!")

try:
    RESULTAT = recupere_indice(valeur=VALEUR, nombres=NOMBRES)
except ValueError:
    print("PROBLEME!")

On voit que d’un côté le style LBYL nécessite presque de coder deux fois la même fonction. D’un autre côté le fait que recupere_indice génère ValueError n’est pas du tout lisible dans sa signature. En fait on ne la même pas mentionné dans la documentation (ce qui est clairement une très mauvaise idée).

Une façon de signaler le problème serait de modifier le type de renvoie pour Union(int, ValueError). Cependant ce n’est pas tout à fait correct. En effet alors qu’on n’a effectivement la possibilité de renvoyer un objet ValueError ici on soulève l’exception ValueError ce qui n’est pas la même chose.