IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
2.
int(input("Entrez un nombre : "))
# Essayez de rentrer une chaîne à ce moment-là…

La seconde façon consiste alors à englober le code « sensible » dans un bloc tryexcept. 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.

 
Sélectionnez
1.
2.
3.
4.
5.
try:
    x=input("Entrez un nombre : ")
    int(x)
except ValueError as e:
    print("Désolé, %s n'est pas un nombre – Exception %s" % (x, e))

Il est possible de gérer plusieurs exceptions de façon distincte…

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
def calcul(n): return n ** 0.5/n

try:
    x=input("Entrez un nombre : ")
    print(calcul(int(x)))
except ValueError as e:
    print("Désolé, %s n’est pas un nombre – Exception %s" % (x, e))
except ZeroDivisionError as e:
    print("Désolé, on ne divise pas par zéro – Exception %s" % e)

… ou de gérer d'un coup plusieurs exceptions en les regroupant dans un tuple.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
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))

C'est relativement équivalent à ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
def calcul(n): return n ** 0.5/n

try:
    x=input("Entrez un nombre : ")
    res=calcul(int(x))
    print("Le résultat du calcul de %d est %d" % (x, res))
except (ValueError, ZeroDivisionError) as e:
    print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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
BaseException
    SystemExit
    KeyboardInterrupt
    GeneratorExit
    Exception
        StopIteration
        StopAsyncIteration
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            DeprecationWarning
            PendingDeprecationWarning
            RuntimeWarning
            SyntaxWarning
            UserWarning
            FutureWarning
            ImportWarning
            UnicodeWarning
            BytesWarning
            ResourceWarning
 
Sélectionnez
BaseException
    SystemExit
    KeyboardInterrupt
    GeneratorExit
    Exception
        StopIteration
        StandardError
            BufferError
            ArithmeticError
                FloatingPointError
                OverflowError
                ZeroDivisionError
            AssertionError
            AttributeError
            EnvironmentError
                IOError
                OSError
                    WindowsError (Windows)
                    VMSError (VMS)
            EOFError
            ImportError
            LookupError
                IndexError
                KeyError
            MemoryError
            NameError
                UnboundLocalError
            ReferenceError
            RuntimeError
                NotImplementedError
            SyntaxError
                IndentationError
                    TabError
            SystemError
            TypeError
            ValueError
                UnicodeError
                    UnicodeDecodeError
                    UnicodeEncodeError
                    UnicodeTranslateError
        Warning
            DeprecationWarning
            PendingDeprecationWarning
            RuntimeWarning
            SyntaxWarning
            UserWarning
            FutureWarning
            ImportWarning
            UnicodeWarning
            BytesWarning

À 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
class MyError(ValueError):
    def __init__(self, v):
        super().__init__()
        self.__v=v
    def __str__(self):
        return "Erreur [%s]" % str(self.__v)
>>>
>>> raise MyError("toto")
__main__.MyError: Erreur [toto]

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 ».


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.