8. Erreurs et exceptions
************************

Jusqu’ici, 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. 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 sont appelées des *exceptions* et ne sont
pas toujours fatales : nous apprendrons bientôt comment les traiter
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 ce qui s’est passé. 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ée.

La partie précédente dans le message d’erreur indique 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 sont pas
affichées.

Vous trouvez la liste des exceptions natives et leur signification
dans Exceptions natives.


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 accepte) ; 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 le type d’exception
  levée correspond à un nom 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 et ne correspond à 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)
et joue alors le rôle de joker. C’est toutefois à utiliser avec
beaucoup de précautions car il est 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 propager l’exception (en
permettant à un appelant de gérer également 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 "try" … "except" accepte également une *clause else*
optionnelle qui, lorsqu’elle est présente, doit se placer après 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 "try" … "except".

Quand une exception intervient, une valeur peut lui être associée, que
l’on appelle *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 avez besoin de savoir si une exception a été levée mais que
vous n’avez pas intention de la gérer, une forme plus simple de
l’instruction "raise" permet de propager 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. Elles sont le plus souvent gardées
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

Most exceptions are defined with names that end in « Error », similar
to the naming of the standard exceptions.

Beaucoup de modules standards définissent leurs propres exceptions
pour signaler les erreurs possibles 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!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   KeyboardInterrupt

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 propagée après que la clause "finally" a été
exécutée.

Dans les vraies applications, la clause "finally" est notamment utile
pour libérer des ressources externes (telles que des fichiers ou des
connexions réseau), quelle qu’ait été l’utilisation de ces ressources.


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.
