Exceptions en Python

Les exceptions sont un mécanisme de développement extrêmement pratique, mais pas forcément clair pour tout le monde. Suite à une petite discussion, voici leur fonctionnement en python3 :

Qu’est-ce qu’une exception ?

Il s’agit d’objets se comportant comme des erreurs de fonctionnement du programme. Toutes les erreurs, en python, sont des exceptions, et sont donc manipulables. Cela sert à pouvoir traiter soi-même les erreurs, au lieu de planter bêtement. Une structure existe pour effectuer ce traitement : son fonctionnement en langue française donnerait ceci :

Bon, effectue ces actions. Si jamais il y a une erreur à l’intérieur, traite-la comme ceci.

En python, ça se traduit par le bloc try … except :

try:
    do_some_actions()
except:
    print('Nous avons une erreur !')

Si le bloc try renvoie une exception (donc une erreur), le bloc except est exécuté. En l’occurrence, on affiche un message, puis on continue le programme. Avec une vraie exception, ça donnerait ça :

try:
    result = 10 / 0 # une division par zéro, c’est paaaaas bien
except:
    print('Nous avons une erreur !')

Cela reste néanmoins un fonctionnement très basique. On ne sait pas, à ce stade, quelle est l’erreur. L’exception étant un objet, pouvoir la manipuler est la base du traitement.

try:
    result = 10 / 0
except Exception, e:
    print('Nous avons une erreur : %s !' % e)

Voici la forme étendue du except : on lui spécifie un type d’exception à gérer, puis un nom de variable, qui contiendra notre exception. L’objet Exception définit la méthode spéciale __str__() appelée automatiquement lorsque l’on cherche à utiliser l’objet comme une chaîne de caractère (comme c’est le cas dans notre exemple), et renvoie l’attribut message. Voici donc le résultat des quelques lignes précédentes :

Nous avons une erreur : integer division or modulo by zero !

Pour savoir quel est le type de notre exception, consultez le nom de la classe : e.__class__.__name__. Ce qui nous permettrait d’effectuer un traitement comme ceci :

try:
    result = 10 / 0
except Exception, e:
    exception_type = e.__class__.__name__
    if exception_type == 'ZeroDivisionError':
        print('Erreur de division par zéro')
    else:
        print('Erreur %s : %s' % (exception_type, e))

Ainsi, vous traiterez différemment votre erreur selon son type. Mais la structure try … except permet de faciliter ça, en conditionnant la récupération des exceptions :

try:
    result = 10 / 0
except ZeroDivisionError, e:
    print('Erreur de division par zéro')
except Exception, e:
    print('Erreur %s : %s' % (e.__class__.__name__, e))

Ici, soit nous récupérons une erreur ZeroDivisionError, soit nous récupérons… n’importe quelle autre erreur, car la récupération conditionnelle traite également les sous-types. Toute exception héritant de Exception, notre except Exception, e saura récupérer n’importe quelle exception, et agit donc comme le default d’un switch. Dans l’exemple précédent, la ligne 4 s’exécutera seulement si on intercepte une exception ZeroDivisionError (ou un type héritant de ZeroDivisionError. Si on obtient n’importe quelle autre exception, on exécute la ligne 6. Si une exception d’un type non-traité est levée, python s’arrêtera avec un traceback et un message d’erreur (qui sera celui de l’exception)

Le bloc inclus dans le try est du code, il n’a rien de particulier. Il est donc évidemment possible d’imbriquer des blocs try … except. Comme ceci :

try:
    do_something()
    try:
        do_something_else()
    except Exception, e:
        raise UserWarning(e.message)

    try:
        do_all_the_things()
    except Exception, e:
        raise UserWarning(e.message)

except UserWarning, e:
    print('Le programme s’est arrêté avec le message suivant : %s' % e)

Si do_something_else() renvoie une exception, celle-ci est récupérée par le except ligne 5, qui renvoie elle-même une exception (avec le mot-clé raise), qui est alors récupérée par le try parent : on entre alors dans le dernier except.

Pour finir, voyons comment créer nos exceptions :

class MyException(Exception):
    pass

C’est aussi simple que ça : créez une classe vide qui hérite d’Exception, ou d’une autre exception (par exemple, pour hiérarchiser vos exceptions), vu que le nom de classe est ce qui permettra de récupérer conditionnellement vos exceptions.

try:
    raise MyException('Message')
except MyException, e:
    print(e)

Notre exception est levée, et est traitable comme une autre.

Une dernière chose : le mot-clé finally, qui est exécuté après une structure try … except, quel que soit le bloc traité, autrement dit peu importe qu’une exception ait été levée ou pas :

try:
    do_something()
except Exception, e:
    print(e)
finally:
    print('Nous avons exécuté le bloc.')

Un finally est utile notamment pour libérer des ressources. Par exemple, si un fichier est ouvert dans le try mais qu’une exception est levée avant sa fermeture, le finally peut le fermer dans tous les cas.