8. Erreurs et exceptions

Jusqu’à maintenant, les messages d’erreurs ont seulement été mentionnés, mais si vous avez essayé les exemples vous avez certainement vu plus que cela. En fait, il y a au moins deux types d’erreurs à distinguer : les erreurs de syntaxe et les exceptions.

8.1. Les erreurs de syntaxe

Les erreurs de syntaxe, qui sont des erreurs d’analyse du code, sont peut-être celles que vous rencontrez le plus souvent lorsque vous êtes encore en phase d’apprentissage de Python :

>>> while True print 'Hello world'
  File "<stdin>", line 1
    while True print 'Hello world'
                   ^
SyntaxError: invalid syntax

L’analyseur répère la ligne incriminée et affiche une petite “flèche” pointant vers le premier endroit de la ligne où l’erreur a été détectée. L’erreur est causée (ou, au moins, a été détectée comme telle) par le symbole placé avant la flèche, ici car il manque deux points (':') avant lui, dans l’exemple l’erreur est détectée au mot clef print, car il manque deux points (':') juste avant. Le nom de fichier et le numéro de ligne sont affichés pour vous permettre de localiser facilement l’erreur lorsque le code provient d’un script.

8.2. Les exceptions

Même si une instruction ou une expression est syntaxiquement correcte, elle peut générer une erreur lors de son exécution. Les erreurs détectées durant l’exécution son appelées des exceptions et ne sont pas toujours fatales : vous apprendrez bientôt comment les traîter dans vos programmes. La plupart des exceptions toutefois ne sont pas prises en charge par les programmes, ce qui génère des messages d’erreurs comme celui-ci :

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

La dernière ligne du message d’erreur indique la cause de l’erreur. Les exceptions peuvent être de différents types, et ce type est indiqué dans le message : les types indiqués dans l’exemple sont ZeroDivisionError, NameError et TypeError. Le texte affiché comme type de l’exception est le nom de l’exception native qui a été déclenchée. Ceci est vrai pour toutes les exceptions natives, mais n’est pas une obligation pour les exceptions définies par l’utilisateur (même si c’est une convention bien pratique). Les noms des exceptions standards sont des identifiants natifs (pas des mots réservés).

Le reste de la ligne fournit plus de détails en fonction du type de l’exception et de ce qui l’a causé.

La partie précédente du message d’erreur montre le contexte dans lequel s’est produite l’exception, sous la forme d’une trace de pile d’exécution. En général, celle-ci contient les lignes du code source ; toutefois, les lignes lues à partir de l’entrée standard ne seront pas affichées.

Vous trouverez dans Built-in Exceptions la liste des exceptions natives et leur signification.

8.3. Gestion des exceptions

Il est possible d’écrire des programmes qui prennent en charge certaines exceptions. Regardez l’exemple suivant, qui demande une saisie à l’utilisateur jusqu’à ce qu’un entier valide ait été entré, mais permet à l’utilisateur d’interrompre le programme (en utilisant Control-C ou un autre raccourci que le système supporte) ; notez qu’une interruption générée par l’utilisateur est signalée en levant l’exception KeyboardInterrupt.

>>> while True:
...     try:
...         x = int(raw_input("Please enter a number: "))
...         break
...     except ValueError:
...         print "Oops!  That was no valid number.  Try again..."
...

L’instruction try fonctionne comme ceci.

  • Premièrement, la clause try (instruction(s) placée(s) entre les mots-clés try et except) est exécutée.
  • Si aucune exception n’intervient, la clause except est sautée et l’exécution de l’instruction try est terminée.
  • Si une exception intervient pendant l’exécution de la clause “try”, le reste de cette clause est sauté. Si son type correspond à un nom d’exception indiqué après le mot-clé except, la clause “except” correspondante est exécutée, puis l’exécution continue après l’instruction try.
  • Si une exception intervient qui ne corresponde à aucune exception mentionnée dans la clause “except”, elle est transmise à l’instruction try de niveau supérieur ; si aucun gestionnaire d’exception n’est trouvé, il s’agit d’une exception non gérée et l’exécution s’arrête avec un message comme indiqué ci-dessus.

Une instruction try peut comporter plusieurs clauses except, pour permettre la prise en charge de différentes exceptions. Mais un seul gestionnaire, au plus, sera exécuté. Les gestionnaires ne prennent en charge que les exceptions qui interviennent dans la clause try correspondante, pas dans d’autres gestionnaires de la même instruction try. Mais une même clause except peut citer plusieurs exceptions sous la forme d’un tuple entre parenthèses, comme dans cet exemple :

... except (RuntimeError, TypeError, NameError):
...     pass

Notez que les parenthèses autour de ce tuple sont nécessaires, car except ValueError, e: est la syntaxe utilisée pour ce qui s’écrit désormais except ValueError as e: dans les dernières versions de Python (comme décrit ci-dessous). L’ancienne syntaxe est toujours supportée pour la compatibilité ascendante. Ce qui signifie que except RuntimeError, TypeError: n’est pas l’équivalent de except (RuntimeError, TypeError): mais de except RuntimeError as TypeError:, ce qui n’est pas ce que l’on souhaite.

La dernière clause except peut omettre le(s) nom(s) d’exception(s), pour servir de joker. C’est toutefois à utiliser avec beaucoup de précautions, car il est très facile de masquer une vraie erreur de programmation par ce biais. Elle peut aussi être utilisée pour afficher un message d’erreur avant de re-lever l’exception (en permettant à un appelant de prendre également en charge l’exception)

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise

L’instruction tryexcept a également une clause else optionnelle qui, lorsqu’elle est présente, doit suivre toutes les clauses except. Elle est utile pour du code qui doit être exécuté lorsqu’aucune exception n’a été levée par la clause try. Par exemple :

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print 'cannot open', arg
    else:
        print arg, 'has', len(f.readlines()), 'lines'
        f.close()

Il vaut mieux utiliser la clause else, plutôt que d’ajouter du code à la clause try, car cela évite de capturer accidentellement une exception qui n’a pas été levée par le code initialement protégé par l’instruction tryexcept.

Quand une exception intervient, une valeur peut lui être associée, que l’on appelle également l’argument de l’exception. La présence de cet argument et son type dépendent du type de l’exception.

La clause except peut spécifier un nom de variable après le nom de l’exception (ou le tuple). Cette variable est liée à une instance d’exception avec les arguments stockés dans instance.args. Pour plus de commodité, l’instance de l’exception définit la méthode __str__() afin que les arguments puissent être imprimés directement sans avoir à référencer .args.

On peut aussi instancier une exception et lui ajouter autant d’attributs que nécessaire avant de la déclencher

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print type(inst)     # the exception instance
...     print inst.args      # arguments stored in .args
...     print inst           # __str__ allows args to be printed directly
...     x, y = inst.args
...     print 'x =', x
...     print 'y =', y
...
<type 'exceptions.Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Si une exception a un argument, il est imprimé dans la dernière partie (“detail”) du message des exceptions non gérées.

Les gestionnaires d’exceptions n’interceptent pas que les exceptions qui sont levées immédiatement dans leur clause try, mais aussi celles qui sont levées au sein de fonctions appelées (parfois indirectement) dans la clause try. Par exemple :

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as detail:
...     print 'Handling run-time error:', detail
...
Handling run-time error: integer division or modulo by zero

8.4. Déclencher des exceptions

L’instruction raise permet au programmeur de déclencher une exception spécifique. Par exemple :

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

Le seul argument à raise indique l’exception à déclencher. Cela peut être soit une instance d’exception, soit une classe d’exception (une classe dérivée de Exception).

Si vous devez savoir qu’une exception a été levée mais sans intention de la prendre en charge, une forme plus simple de l’instruction raise permet de re-déclencher l’exception :

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print 'An exception flew by!'
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere

8.5. Exceptions définies par l’utilisateur

Les programmes peuvent nommer leur propres exceptions en créant de nouvelles classes (voir Classes à propos des classes Python). Ces exceptions doivent typiquement être dérivées de la classe Exception, directement ou indirectement. Par exemple :

>>> class MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
...
>>> try:
...     raise MyError(2*2)
... except MyError as e:
...     print 'My exception occurred, value:', e.value
...
My exception occurred, value: 4
>>> raise MyError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.MyError: 'oops!'

Dans cet exemple, la méthode __init__() de la classe Exception a été surchargée. Le nouveau comportement crée simplement l’attribut value. Ceci remplace le comportement par défaut qui crée l’attribut args.

Les classes d’exceptions peuvent être définies pour faire tout ce qu’une autre classe peut faire. Mais elles sont le plus souvent assez simples, n’offrant que les attributs permettant aux gestionnaires de ces exceptions d’extraire les informations relatives à l’erreur qui s’est produite. Lorsque l’on crée un module qui peut déclencher plusieurs types d’erreurs distincts, une pratique courante est de créer une classe de base pour l’ensemble des exceptions définies dans ce module, et de créer des sous-classes spécifiques d’exceptions pour les différentes conditions d’erreurs :

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expr -- input expression in which the error occurred
        msg  -- explanation of the error
    """

    def __init__(self, expr, msg):
        self.expr = expr
        self.msg = msg

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        prev -- state at beginning of transition
        next -- attempted new state
        msg  -- explanation of why the specific transition is not allowed
    """

    def __init__(self, prev, next, msg):
        self.prev = prev
        self.next = next
        self.msg = msg

La plupart des exceptions sont définies avec des noms qui se terminent par « Error », comme pour les exceptions standards.

La plupart des modules standards définissent leurs propres exceptions pour décrire les erreurs que l’on peut rencontrer dans les fonctions qu’ils définissent. Plus d’informations sur les classes sont présentées dans le chapitre Classes.

8.6. Définition d’actions de nettoyage

L’instruction try a une autre clause optionnelle qui est destinée à définir des actions de nettoyage devant être exécutées dans certaines circonstances. Par exemple :

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print 'Goodbye, world!'
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

La clause finally est toujours exécutée avant de sortir de l’instruction try, qu’une exception soit survenu ou non. Lorsqu’une exception est survenue dans la clause try et n’a pas été attrapée par un except (ou qu’elle s’est produite dans un except ou else), elle sera relancée après la fin de l’exécution de la clause finally. La clause finally est aussi exécutée « en sortant » lorsque n’importe quelle autre clause du try est interrompue que ce soit avec un break, continue ou return. Un exemple plus compliqué (ayant un except et un finally dans le même try fonctionne comme en Python 2.6) :

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print "division by zero!"
...     else:
...         print "result is", result
...     finally:
...         print "executing finally clause"
...
>>> divide(2, 1)
result is 2
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Comme vous pouvez le voir, la clause finally est exécutée dans tous les cas. L’exception de type TypeError déclenchée en divisant deux chaînes de caractères n’est pas prise en charge par la clause except et est donc re-déclenchée après que la clause finally ait été exécutée.

Dans les vraies applicatons, la clause finally est notamment utile pour libérer des ressources externes (telles que des fichiers ou des connections réseau), que l’utilisation de ces ressources ait réussi ou non.

8.7. Actions de nettoyage prédéfinies

Certains objets définissent des actions de nettoyage standards qui doivent être exécutées lorsque l’objet n’est plus nécessaire, indépendamment du fait que l’opération ayant utilisé l’objet ait réussi ou non. Regardez l’exemple suivant, qui tente d’ouvrir un fichier et d’afficher son contenu à l’écran :

for line in open("myfile.txt"):
    print line,

Le problème avec ce code est qu’il laisse le fichier ouvert pendant une durée indéterminée après que le code ait fini de s’exécuter. Ce n’est pas un problème avec des scripts simples, mais peut l’être au sein d’applications plus conséquentes. L’instruction with permet d’utiliser certains objets comme des fichiers d’une façon qui assure qu’ils seront toujours nettoyés rapidement et correctement :

with open("myfile.txt") as f:
    for line in f:
        print line,

Dès que l’instruction est exécutée, le fichier f est toujours fermé, même si un problème est intervenu pendant l’exécution de ces lignes. D’autres objets qui fournissent des actions de nettoyage prédéfinies l’indiquent dans leur documentation.