try – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Pourquoi utiliser un mécanisme d’exceptions ? http://sametmax.com/pourquoi-utiliser-un-mecanisme-dexceptions/ http://sametmax.com/pourquoi-utiliser-un-mecanisme-dexceptions/#comments Wed, 06 Jan 2016 00:17:27 +0000 http://sametmax.com/?p=17365 L’article ne vient pas de nulle part. En fait j’ai dû dans la même journée répondre sur le subreddit Python à cette question et à un lecteur par email, alors je me suis dit qu’il serait bon de faire le point ici.

Avant de lire l’article, assurez-vous de bien avoir compris la gestion des erreurs en Python.

La réponse simple, courte, et définitive

“Pourquoi utiliser un mécanisme d’exceptions ?”

Parce que c’est la manière de faire de la communauté du langage que vous utilisez.

Si vous êtes dans un langage qui utilise des codes de retour comme C, alors gérez les erreurs avec des codes de retour. Si vous avez du pattern matching comme en Rust, utilisez le pattern matching. Si vous avez des exceptions comme en Java, utilisez les exceptions. Si vous avez des guards comme en swift, vous utilisez des guards.

Je crois que vous avez pigé.

Ceci est la règle, qui est complètement indépendante de la qualité du système de gestion des erreurs : codez dans le style approprié pour le langage et n’essayez pas de bricoler votre solution perso dans votre coin. Si ça vous fait chier, changez de langage. Mais ne faites pas du pseudo-Go en Erlang, ça n’a pas de sens, et ça va saoûler tous vos collègues, en plus de diminuer l’intégration avec l’écosystème de la techno.

En Python, les erreurs (et même plus) sont gérées via un mécanisme à base d’exceptions, et c’est donc ce qu’il faut utiliser.

Maintenant, un mécanisme de gestion d’erreurs est vraiment une question de goût. Il n’y en a pas de parfait, mais il peut être intéressant de connaitre les points forts de celui qu’utilise Python.

Quels sont donc les points forts d’un mécanisme à base d’exceptions ?

Cela évite les races conditions

En informatique, dès qu’un système peut faire plusieurs choses à la fois, plusieurs choses peuvent arriver simultanément et créer ce qu’on appelle une race condition.

Par exemple, si je fais ceci:

# on vérifie que le fichier existe avant de l'ouvrir
if os.path.isfile('monfichier'): 
    with open('monfichier') as f:
        print(f.read())
else:
    print('pouet')

Entre la première ligne et la seconde ligne s’écoule un temps très court pendant lequel un autre processus peut supprimer le fichier et faire planter mon programme.

En utilisant la philosophie “il est plus facile de demander pardon que la permission”, on évite ce problème:

try:
    # on s'en bat les couilles, on essaye
    # de l'ouvrir à sec
    with open('monfichier') as f:
        print(f.read())
except OSError:
    print('pouet')

Dans ce cas on tente d’ouvrir le fichier de toute façon, et si l’ouverture déclenche une erreur, alors on la gère. Pas de race condition.

Finally et with pour les opérations de nettoyage

En ouvrant mon fichier, je dois m’assurer de le fermer après. Mais je peux avoir une erreur à l’ouverture du fichier, ou pendant sa lecture, qui fasse planter mon programme et que je n’avais pas prévue.

Comme les exceptions remontent la file d’appel, on peut les attraper à plusieurs niveaux. Grâce à finally (et with qui enrobe finally), on peut donc très élégamment s’assurer que les opérations de nettoyage sont lancées automatiquement, même si tout pête:

# ouvrir un fichier avec with garantit sa fermeture
with open('monfichier') as f:
    print(f.read())

Les exceptions sont très explicites

Des mécanismes comme le pattern matching ou le retour de codes sont génériques, et peuvent être utilisés pour à peu près tout.

Les exceptions, à l’image des guards, ont un champ d’usage plus restreint, et quand on en voit, on sait donc généralement à quoi s’en tenir. Cela facilite la lecture en diagonale du code.

Les exceptions décrivent une hiérarchie d’erreurs

Ceci permet non seulement de choisir de gérer plusieurs erreurs d’un coup, ou alors séparément, mais également de documenter par le type quel est le problème. Une fois qu’on connait les exceptions les plus courantes en Python (ValueError, OSError, KeyError, TypeError, etc.), on identifie vite l’idée générale d’un message d’erreur ou d’un code attrapant une erreur.

Comme on peut créer ses propres types d’exceptions, on peut permettre le ciblage des erreurs d’une lib en particulier, ou d’un sous ensemble d’une lib ou d’une opération. Et c’est aussi une forme de documentation par le code.

Tout cela autorise une fine granularité sur ce qu’on veut gérer ou pas : tout d’un coup, au cas par cas, seulement sur une partie du code, etc.

Les exceptions bubblent

Sous ce terme barbare se cache le fait que les exceptions se déclenchent localement dans un bloc de code, et l’interrompent, mais ne font pas planter le programme tout de suite. À la place, l’exception monte d’un niveau dans la file d’appels, et casse tout, puis remonte d’un cran, et casse tout, et ainsi de suite, jusqu’en haut.

Ce mécanisme permet de choisir exactement où on veut arrêter l’exception, et ce que l’on souhaite qu’elle puisse interrompre. Cela laisse le choix de gérer des erreurs de manière macroscopique ou microscopique.

Par exemple, si j’utilise un try en dehors d’une boucle :

print('start')
try:
    for x in range(0, 10):
        print(1 / (x - 2))
except ZeroDivisionError:
    pass

print('fin')
## Affiche :
## start
## -0.5
## -1.0
## fin

Et un dans une boucle :

print('start')
for x in range(0, 10):
    try:
        print(1 / x)
    except ZeroDivisionError:
        print('ERROR !')
    else:
        print("Pas d'erreur :)")
    finally:
        print('TOUJOURS')
print('fin')
## Affiche:
## start
## ERROR !
## TOUJOURS
## 1.0
## Pas d'erreur :)
## TOUJOURS
## 0.5
## Pas d'erreur :)
## TOUJOURS
## 0.3333333333333333
## Pas d'erreur :)
## TOUJOURS
## 0.25
## Pas d'erreur :)
## TOUJOURS
## 0.2
## Pas d'erreur :)
## TOUJOURS
## 0.16666666666666666
## Pas d'erreur :)
## TOUJOURS
## 0.14285714285714285
## Pas d'erreur :)
## TOUJOURS
## 0.125
## Pas d'erreur :)
## TOUJOURS
## 0.1111111111111111
## Pas d'erreur :)
## TOUJOURS
## fin

J’obtiens un résultat radicalement différent. On peut choisir facilement l’étendue de la propagation de l’erreur.

Les exceptions ont des données attachées

Les exceptions sont des objets riches, qui viennent avec un message d’erreur, un contexte qui permet de générer une stack trace, et parfois des attributs en plus comme le code d’erreur fourni par l’OS.

C’est un point d’entrée capable de concentrer en un seul endroit tout ce dont on a besoin pour le debug.

Pas cher mon fils

En Python, les exceptions sont particulièrement peu couteuses à utiliser. En fait, Python les utilise pour le contrôle de flux (StopIteration, GeneratorExit, etc) donc elles sont au coeur du fonctionnement du langage, et pas juste pour les erreurs.

Faire un try/except n’a pas le coût qu’on a en Java ni en terme de performance du code, ni en terme de verbosité car il n’y a pas de throw à déclarer.

Le truc le plus ennuyeux, c’est bien entendu de trouver le nom de l’exception qu’on veut gérer et comment l’importer. Afin d’éviter cette chose affreuse:

    try:
        meh()
    except: # YOLO
        print("Il s'est passé un truc, mais je sais pas quoi")

Il y a à peine quelques heures j’étais avec un client qui avait des utilisateurs se plaignant que le système ne marchait pas sans vraiment pouvoir diagnostiquer pourquoi.

Vous voyez, pas besoin d’inventer, j’ai les exemples qui me tombent tout cru dans le bec.

Voici ce qu’il avait, en prod :

# je vous pseudo code, mais c'est l'idée
def get_dbs():
    try:
        # connection, listing, filtrage, casting
        # nahhh, que pourrait-il arriver de grave ?
        con = Connect()
        dbs = con.get_databases()
        dbs = fitler_system_db(dbs)
        return tuple(dbs)
    except: # double combo: catch all ET silence
        return ()

Oui, j’ai bien dit en prod. La base de données gère des listings de médecins. Ouch.

Le code utilisant cette fonction faisait (avant que j’arrive avec mon fouet de zorro et corrige tout ce bordel):

dbs = get_dbs()
if not dbs:
    display_error("Mongo ne tourne pas, ou la connection a échoué, ou aucune table n'est créé")

Vous imaginez comme l’utilisateur final pouvait facilement décrire son problème… Ouai alors soit t’as pas un truc, soit le truc marche pas, soit tu te sers pas du truc. Ouai ça couvre à peut prêt l’ensemble des erreurs possibles sur tous systèmes, comme ça on peut pas avoir fondamentalement tort.

EDIT:

Histoire de mettre fin au débat sur le code final qui va immanquablement envahir les coms:

  • La connexion n’a juste rien à foutre dans la fonction qui liste les dbs. Cela doit être deux fonctions séparées.
  • On attrape pas d’exception au niveau de la connexion. On attrape une exception au niveau du code qui déclenche la connexion afin de pouvoir donner un rapport d’erreur à l’utilisateur depuis le code qui gère l’UI.
  • On attrape pas une exception, mais 3-4, avec des messages d’erreur distincts pour chaque cas donnant une action claire à l’utilisateur qu’il puisse mettre en œuvre pour résoudre ce problème en particulier.
  • Oui, on peut retourner un tuple vide pour indiquer qu’on a rien trouvé (comme ça on retourne toujours un itérable) mais certainement pas pour signaler une erreur.
  • La fonction ne permet pas d’être unit testée facilement car non seulement elle fait trop de choses, mais en plus elle a plein d’entrées implicites (settings de connexion, liste des choses à fitrer, etc.).

Bref, faut tout réécrire. Ça tombe bien, il me paie pour ça.

Notez que face à ce genre de code, le comportement à adopter n’est pas de mettre la misère à son client, mais simplement de lister les améliorations possibles, les justifier et estimer le coût et les bénéfices des changements. On est pas la pour fanfaronner, juste pour s’assurer qu’il puisse faire son taff.

Là je me permet de faire le clown parce que ça rend l’article plus léger, et parce qu’il est américain, et ne sera donc pas impacté par l’article. De l’intérêt également d’être un blogger anonyme et francophone…

]]>
http://sametmax.com/pourquoi-utiliser-un-mecanisme-dexceptions/feed/ 20 17365
Gestion des erreurs en Python http://sametmax.com/gestion-des-erreurs-en-python/ http://sametmax.com/gestion-des-erreurs-en-python/#comments Tue, 20 Jan 2015 17:46:12 +0000 http://sametmax.com/?p=15786 Plus on code, plus on oublie ce que c’était quand on a débuté. Même moi, et pourtant je fais un gros effort pour essayer de me replonger dans cet état d’esprit.

Dernièrement j’ai eu plusieurs interactions qui m’ont mis un petit taquet derrière la tête :

– “Mec, les exceptions, y a plein de personnes qui pigent pas.”

– “Nannnn, mais quand même, les exceptions…”

– “Si, les exceptions.”

– “Nan, vraiment ?”

– “Oui, vraiment.”

Donc, back to black, et petit tour de la gestion d’erreur en Python.

Et de la musique.

La notion d’exception

Une exception est un mécanisme d’interruption du programme utilisé pour signaler que quelque chose d’anormal est en train de se produire.

On les rencontre dans de nombreux cas, mais souvent, c’est dans le cadre d’erreurs. Par exemple, Python va lever une exception dans les cas suivant :

>>> 1 / 0 # division par zéro

Traceback (most recent call last):
  File "", line 1, in 
    1 / 0
ZeroDivisionError: integer division or modulo by zero

>>> l = [1, 2, 3]
>>> l[100] # dépassement d'un tableau

Traceback (most recent call last):
  File "", line 1, in 
    l[100]
IndexError: list index out of range

>>> d = {'cle': 'valeur'}
>>> d['nope'] # clé de dico inconnue

Traceback (most recent call last):
  File "", line 1, in 
    d['nope']
KeyError: 'nope'

>>> 1 + "banane" # opération entre types incompatibles

Traceback (most recent call last):
  File "", line 1, in 
    1 + "banane"
TypeError: unsupported operand type(s) for +: 'int' and 'str'

>>> import nawak # ce module n'existe pas

Traceback (most recent call last):
  File "", line 1, in 
    import nawak
ImportError: No module named nawak

>>> open('nawak') # le fichier n'existe pas

Traceback (most recent call last):
  File "", line 1, in 
    open('nawak')
IOError: [Errno 2] No such file or directory: 'nawak'

>>> print(nawak) # la variable n'existe pas
Traceback (most recent call last):
  File "", line 1, in 
    print(nawak)
NameError: name 'nawak' is not defined

>>> n a w a k # erreur de syntaxe
  File "", line 1
    n a w a k # erreur de syntaxe
      ^
SyntaxError: invalid syntax

>>> int('a') # la valeur passée n'a aucun sens

Traceback (most recent call last):
  File "", line 1, in 
    int('a')
ValueError: invalid literal for int() with base 10: 'a'

De base, il y a un paquet d’exceptions en Python, et celles fournies par défaut sont listées ici.

Malgré l’abondance de ces exceptions, vous pouvez remarquer un motif récurrent quand Python crash et affiche l’erreur :

Gros pâté de texte.
Gros pâté de texte.
Gros pâté de texte.
NomDeLExceptionError: description de l'erreur.

En effet, non seulement une exception interrompt le programme, mais elle collecte des informations sur la source de l’erreur afin qu’au moment où ça crash, le développeur ait de quoi déboguer.

Une exception va donc normalement déclencher un affichage à la fin avec toutes ces infos, et sur la dernière ligne, le type de l’exception (ValueError, IndexError, etc) ainsi qu’une description de ce qui a causé l’erreur.

Néanmoins, le plus intéressant est ce qu’il y a au dessus : le gros pâté de texte. C’est ce qu’on appelle une stack trace, et ça représente la pile d’appels qui ont amené à cette erreur. Chaque ligne de la stack trace va vous donner le chemin d’un fichier de code, et une ligne.

Une erreur se lit donc à l’envers, de bas en haut.

Vous lisez d’abord le nom de l’erreur et sa cause, puis, vous remontez la stack trace ligne à ligne, pour essayer de trouver quelle ligne de quel fichier de code vous devez déboguer.

Par exemple, si vous avez un fichier wololo.py :

def une_fonction():
    return 1 / 0

def une_autre_fonction():
    une_fonction()

une_autre_fonction()

Lancer ce script va vous pondre :

Traceback (most recent call last):
  File "wololo.py", line 7, in 
    une_autre_fonction()
  File "wololo.py", line 5, in une_autre_fonction
    une_fonction()
  File "wololo.py", line 2, in une_fonction
    return 1 / 0
ZeroDivisionError: integer division or modulo by zero

On le lit de bas en haut :

ZeroDivisionError: integer division or modulo by zero

Ok, donc c’est une erreur de division par 0.

  File "wololo.py", line 2, in une_fonction
    return 1 / 0

Elle a lieu dans mon fichier wololo.py à la ligne 2.

Mais visiblement, ce qui déclenche tout le bordel, c’est que la fonction contenant cette ligne est appelé à la ligne 5 :

  File "wololo.py", line 5, in une_autre_fonction
    une_fonction()

Qui est elle même appelée à la ligne 7 :

  File "wololo.py", line 7, in 
    une_autre_fonction()

Avec ces infos, on peut prendre une décision : aller ligne 2 pour corriger un bug, ou aller ligne 5, ou 7 pour retirer l’appel qui cause le déclenchement du code fautif.

Dans ce cas, vous pourriez vous dire qu’il faut retirer la division par 0, et que les premières ligne de la stack trace ne servent à rien.

Mais imaginez que je change mon code :

def une_fonction(diviseur):
    return 1 / diviseur

def une_autre_fonction():
    une_fonction(diviseur=0)

une_autre_fonction()

A ce moment là, mon stack trace sera :

Traceback (most recent call last):
  File "wololo.py", line 7, in 
    une_autre_fonction()
  File "wololo.py", line 5, in une_autre_fonction
    une_fonction(diviseur=0)
  File "wololo.py", line 2, in une_fonction
    return 1 / diviseur
ZeroDivisionError: integer division or modulo by zero

Et là, on ne veut pas changer la ligne 2, on veut changer la ligne 5, car c’est ce paramètre qui fait qu’à la ligne 2 on a une division par 0.

La stack trace permet donc de remonter la chaîne de causalité de vos erreurs.

Lever ses propres exceptions

Quand une exception est activée, on dit qu’elle est “levée”.

Python va lever des exceptions de lui-même, par exemple en cas d’erreurs, mais on peut aussi lever soi-même des exceptions en faisant raise NomDeLException("Message").

Par exemple, vous avez une fonction qui n’accepte en paramètre que comme valeur 1, 2 ou 3. Vous savez que si un utilisateur rentre quelque chose d’autre, ça va tout faire merder.

Vous pouvez faire ceci :

def votre_super_fonction(param):
    if param not in (1, 2, 3):
        raise ValueError("'param' can only be either 1, 2 or 3")
    # reste du code

Ainsi, si quelqu’un passe un mauvais paramètre, il est immédiatement alerté :

>>> votre_super_fonction(4)
Traceback (most recent call last):
  File "", line 1, in 
    votre_super_fonction(4)
  File "", line 3, in votre_super_fonction
    raise ValueError("'param' can only be either 1, 2 or 3")
ValueError: 'param' can only be either 1, 2 or 3

Attention : le message ne peut pas contenir de caractères non-ASCII. Oubliez les accents.

Attraper une exception

Les exceptions ne sont pas un simple mécanisme de debuggage. Elles servent d’abord et avant tout à gérer les cas exceptionnels, et on peut donc les détecter, et réagir quand elles surviennent, à l’aide de l’instruction try/except.

Cela s’utilise en enrobant les lignes qui peuvent lever une exception :

    try:
        # lignes qui peuvent
        # lever une exception
    except NomException:
        # faire un truc si l'exception se déclenche

Quand vous attrapez une exception, le programme ne plante pas. A la place, le bloc except correspondant au nom de l’exception est appelée.

Bien entendu, si une exception qui ne porte pas ce nom est levée, le programme plante.

Par exemple :

    # i est définit plus haut
    personnages = ['iron', 'super', 'bat', 'clepto']
    try:
        resultat = personnages[i]
    # i est plus grand que la taille du tableau
    except IndexError:
        resultat = None

Si i est plus grand que la taille de la liste, Python va lever une IndexError, qui sera attrapée. Le programme ne plantera pas, et resultat sera égal à None.

Si i est de type string, Python va lever une TypeError, et le programme va planter.

Cela suppose de savoir le nom de l’erreur qu’on veut gérer. Je recommande donc de tester le cas foireux dans le shell auparavant.

On peut gérer plusieurs exceptions avec un seul bloc :

    try:
        return personnages[100 / i]
    # est exécuté pour ces deux exceptions
    except (IndexError, ZeroDivisionError):
        return None
    # est exécuté pour cette exception
    except KeyError:
        print("Qui est le bâtard qui a remplacé ma "
              "liste par un dico dans mon dos ?")

Hiérarchie des exceptions

Les exceptions sont des classes. Et si vous avez lu notre dossier sur la POO, vous savez que les classes peuvent hériter d’autres classes.

C’est le cas des exceptions. Celles intégrées à Python suivent la hiérarchie suivante :

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

Par exemple, IndexError et KeyError héritent toutes les deux de LookupError.

Donc si vous faites :

    try:
        return personnages[i]
    except LookupError:
        return None

Vous attrapez à la fois les exceptions de type IndexError et KeyError.

LookupError hérite de Exception, dont hérite la plupart des exceptions en Python. Donc si vous faites :

    try:
        return personnages[100 / i]
    except Exception:
        return None

Vous allez attraper LookupError, IndexError, ZeroDivisionError et probablement tout un tas de trucs auxquels vous n’avez pas pensé ce qui peut potentiellement un jour masquer un bug dans votre programme.

Donc gérez toujours l’erreur au plus proche de la logique de votre programme.

Il est d’ailleurs possible de dire à Python “tu m’attrapes tout” en omettant le nom de l’exception :

    try:
        return personnages[100 / i]
    except:
        return None

C’est généralement une mauvaise idée car si il arrive une erreur que vous n’avez pas prévue, la close except s’activera, masquant l’erreur, et ne vous donnera jamais la chance de découvrir le problème.

Sachez également que vous n’êtes pas limité à la hiérarchie offerte par Python. Vous pouvez créer vos propres exceptions en héritant d’une des exceptions existantes de Python :

class MonProjetError(Exception)
    pass

class MonTraitementError(MonProjetError)
    pass

C’est généralement une bonne idée quand vous créez une lib et que vous voulez permettre aux utilisateurs de votre bibliothèque de pouvoir filtrer les exceptions levées par votre code uniquement.

Néanmoins, dans ce cas, choisissez intelligemment les parents de vos exceptions personnalisées. Chaque exception de Python est porteuse de sens. TypeError est levée pour une erreur de type, IOError pour une erreur d’entrée/sortie, etc. Il est logique qu’un développeur s’attende à pouvoir les attraper selon ce sens.

Heureusement, on peut hériter de plusieurs exceptions :

class MonProjetError(Exception)
    pass

class MonTraitementError(MonProjetError, TypeError)
    pass

Du coup, si quelqu’un essaye d’attraper MonTraitementError ou TypeError ou MonProjetError selon ce qu’il désire exprimer, ça marchera.

En effet, les exceptions sont une forme d’expression :

  • Celui qui lève l’exception dit explicitement ce qui peut merder : quand on lit son code, on comprend les cas d’erreurs.
  • Celui qui l’attrape dit explicitement ce qu’il veut gérer : quand on lit son code, on comprend l’objectif.
  • Python dit explicitement ce qui a foiré. Quand on lit la stack trace, on comprend ce qui a merdé.

Vous communiquez en utilisant des exceptions entre développeurs et utilisateurs du code.

Finally et else

try/except peut être complété par deux autres mots clés : finally et else.

else est le bloc exécuté si aucune exception n’est levée :

    try:
        return personnages[100 / i]
    except (IndexError, ZeroDivisionError):
        return None
    except KeyError:
        print("Qui est le bâtard qui a remplacé ma "
              "liste par un dico dans mon dos ?")
    else:
        print('Bon en fait tout va bien')

finally est un bloc qui est exécuté après que tous les autres blocs aient été exécutés, peu importe qu’il y ait eu une exception ou non, et même si le programme crash.

    try:
        return personnages[100 / i]
    except (IndexError, ZeroDivisionError):
        return None
    except KeyError:
        print("Qui est le bâtard qui a remplacé ma "
              "liste par un dico dans mon dos ?")
    finally:
        print('Rosebud !')

Dans notre cas, peu importe ce qui se passe dans le bloc try/except, Rosebud ! est toujours affiché.

finally n’est pas à l’épreuve des balles, si on crash l’interpréteur violemment (ou que vous débranchez la prise du serveur :)), il ne vous sauvera pas. Mais il permet de gérer la plupart des cas extrêmes.

Ah, oui, au fait, si vous utilisez finally, except n’est pas obligatoire.

Quand utiliser try, except, else et finally ?

On utilisera try/except d’abord et avant tout pour gérer les erreurs sur laquelle on a pas de controle.

Par exemple, vous téléchargez un document depuis le Web :

>>> import urllib.request
>>> fichier_recu, headers = urllib.request.urlretrieve('http://www.sudinfo.be/sites/default/files/imagecache/pagallery_450x300/1365338424_B9765144Z.1_20121219124926_000_GOJDROM6.1-0.png')

Ca va marcher la plupart du temps. Et puis un jour, le réseau va sauter, et ça va plus marcher :

>>> fichier_recu, headers = urllib.request.urlretrieve('http://www.sudinfo.be/sites/default/files/imagecache/pagallery_450x300/1365338424_B9765144Z.1_20121219124926_000_GOJDROM6.1-0.png')
Traceback (most recent call last):
  File "", line 1, in 
    fichier_recu, headers = urllib.request.urlretrieve('http://www.sudinfo.be/sites/default/files/imagecache/pagallery_450x300/1365338424_B9765144Z.1_20121219124926_000_GOJDROM6.1-0.png')
  File "/usr/lib/python3.4/urllib/request.py", line 178, in urlretrieve
    with contextlib.closing(urlopen(url, data)) as fp:
  File "/usr/lib/python3.4/urllib/request.py", line 153, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib/python3.4/urllib/request.py", line 455, in open
    response = self._open(req, data)
  File "/usr/lib/python3.4/urllib/request.py", line 473, in _open
    '_open', req)
  File "/usr/lib/python3.4/urllib/request.py", line 433, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.4/urllib/request.py", line 1258, in http_open
    return self.do_open(http.client.HTTPConnection, req)
  File "/usr/lib/python3.4/urllib/request.py", line 1235, in do_open
    raise URLError(err)
URLError:

On va donc gérer l’exception (et beaucoup d’autres, le réseau ça foire tout le temps) :

try:
    fichier_recu, headers = urllib.request.urlretrieve('http://www.sudinfo.be/sites/default/files/imagecache/pagallery_450x300/1365338424_B9765144Z.1_20121219124926_000_GOJDROM6.1-0.png')
except urllib.URLError:
    # faire un truc

Parmi les choses à faire : réessayer plus tard, tenter de redémarrer la carte réseau, changer d’URL…

Un autre raison d’utiliser try/except est pour enregistrer une trace des erreurs.

try:
    url = 'http://www.sudinfo.be/sites/default/files/imagecache/pagallery_450x300/1365338424_B9765144Z.1_20121219124926_000_GOJDROM6.1-0.png'
    fichier_recu, headers = urllib.request.urlretrieve(url)
except urllib.URLError:
    with open('/tmp/errors.log', 'a') as f:
        f.write("L'URL '%s' est inacessible" % url)

On peut le faire avec le module logging, ou envoyer un mail ou un SMS d’alerte…

Proposer un comportement par défaut est aussi très courant.

Par exemple, vous voulez récupérer le premier élément d’une liste, mais si la liste est vide, récupérer None :

try:
    resultat = liste[0]
except IndexError:
    resultat = None

Ce cas d’usage est typique de Python. Dans d’autres langage, on regarderait si le tableau est vide :

if liste:
    resultat = liste[0]
else:
    resultat = None

C’est typiquement la philosophie Java : “Watch before you leap” (WBYL). Soit “Regardez où vous mettez les pieds”.

En Python, la philosophie est “It’s easier to ask for forgiveness than permission” (EAFP). Soit “il est plus facile de demander pardon que la permission”.

La raison est que les exceptions sont très rapides en Python, et permettent de gérer plein de cas d’un coup.

Par exemple, dans le cas :

if conteneur:
    resultat = conteneur[0]
else:
    resultat = None

Que se passe-t-il si conteneur peut être un dictionnaire ou une liste ? C’est possible en Python du fait du duck typing.

Il faut tester deux cas :

if conteneur and 0 in conteneur:
    resultat = conteneur[0]
else:
    resultat = None

Avec une exception, on teste un seul cas :

try:
    resultat = conteneur[0]
except LookupError:
    resultat = None

Là c’est un cas simple, mais sur des cas complexes comme les fichiers, c’est très utile :

try:
    fichier = open('/tmp/fichier', 'w')
except (IOError, OSError):
    # gérer l'erreur

Si je devais faire ça avec des if, il faudrait :

  • Vérifier que le fichier existe.
  • Vérifier que le fichier est un fichier et non un dossier.
  • Vérifier que j’ai les permissions d’écrire sur le fichier.
  • Vérifier que j’ai les permissions de traverser les dossiers parents.
  • Vérifier que personne n’a ouvert le fichier en écriture avant.

C’est relou. Mais en prime, entre chaque vérification du if s’écoule quelques nanosecondes durant lequelles l’état de mon fichier a pu changer. Un autre programme a pu changer les permissions, créer un fichier avec le même nom, etc. Arrivé à la fin du if, on n’a pas la garantie que les vérifications du début du if sont toujours d’actualité.

try/except évite ce problème qu’on appelle des “race conditions” : on essaye d’abord, et ensuite si ça foire, on analyse pourquoi. Comme en drague.

Enfin on va utiliser finally pour faire du nettoyage : fermer un fichier, fermer une connexion à une base de données, une socket, supprimer un fichier temporaire. Bref, tout ce qu’on veut qui arrive, même si notre code se vautre.

try:
    fichier = open('/tmp/fichier', 'w')
except (IOError, OSError):
    # gérer l'erreur
else:
    # faire un truc avec le fichier

# on essaye toujours de fermer notre fichier
finally:
    try:
        fichier.close()

    # le fichier n'a jamais été ouvert et
    # la variable n'existe pas
    except NameError:
        pass

with

Comme vous avez pu le constater, gérer proprement les erreurs, ça peut devenir vite chiant. Pour cette raison, beaucoup de context managers servent à gérer les cas les plus courants.

Typiquement :

try:
    fichier = open('/tmp/fichier', 'w')
except (IOError, OSError):
    # gérer l'erreur
else:
    # faire un truc avec le fichier
finally:
    try:
        fichier.close()
    except NameError:
        pass

Peut être remplacé par :

try:
    with open('/tmp/fichier', 'w') as fichier:
        # faire un truc avec le fichier
except (IOError, OSError):
    # gérer l'erreur

Ou même en Python 3:

try:
    with open('/tmp/fichier', 'w') as fichier:
        # faire un truc avec le fichier
except EnvironmentError:
    # gérer l'erreur

Ce qui est quand même vachement plus court. Le context manager va s’occuper de la fermeture du fichier pour nous proprement. Cet article est déjà long, donc je ne vais pas rentrer en détail sur with, mais sachez que beaucoup d’objets qui demandent une fermeture peuvent être traités comme ça.

Bubbling

Une chose qui échappe généralement aux débutants quand ils commencent à utiliser les exceptions, c’est qu’elles bubblent, c’est à dire qu’elles remontent comme une bulle, de bloc en bloc, jusqu’à la racine du programme.

Si j’ai :

if truc:
    if machin:
        for bidule in chose:
            raise MaisAieuuuuuError("Ca fait mal t'es con")

Mon programme ne va PAS planter à la ligne raise MaisAieuuuuuError("Ca fait mal t'es con").

A la place, l’erreur va remonter en dehors de la boucle. Si elle n’est pas attrapée, elle va remonter en dehors du premier if. Si elle n’est pas attrapée, elle va remonter en dehors du second if, et enfin, si elle vraiment, mais alors vraiment pas attrapée, Python va crasher, et montrer une belle stack trace.

Ça veut dire que vous pouvez arrêter l’exception à plusieurs endroits dans le programme :

if truc:
    if machin:
        for bidule in chose:
            try:
                raise MaisAieuuuuuError("Ca fait mal t'es con")
            except MaisAieuuuuuError:
                # faire un truc

Dans ce cas, la boucle va continuer à tourner après la première erreur, et faire un try/except à chaque tour de boucle.

Mais si vous faites :

if truc:
    if machin:
        try
            for bidule in chose:
                raise MaisAieuuuuuError("Ca fait mal t'es con")
        except MaisAieuuuuuError:
            # faire un truc

A la première erreur, la boucle est interrompue définitivement. Par contre la condition if machin va continuer sa route.

Mais si vous faites :

if truc:
    try:
        if machin:
            for bidule in chose:
                raise MaisAieuuuuuError("Ca fait mal t'es con")
    except MaisAieuuuuuError:
        # faire un truc

La condition if machin est définitivement interrompue.

Et si vous faites :

try:
    if truc:
        if machin:
            for bidule in chose:
                raise MaisAieuuuuuError("Ca fait mal t'es con")
except MaisAieuuuuuError:
    # faire un truc

Là, tout le programme est interrompu. Ca ne plantera pas, mais ça ne fera plus rien.

Il faut donc choisir soigneusement où attraper l’exception selon la partie du programme qu’on souhaite interrompre.

Manipuler l’exception attrapée

Les exceptions sont des objets complets. On peut les récupérer avec le mot clés as et analyser leur contenu.

Exemple :

try:
    f = open("nawak")
except IOError as e:
    print("args: ", e.args)
    print("errno: ", e.errno)
    print("filename: ", e.filename)
    print("strerror: ", e.strerror)

## args:  (2, 'No such file or directory')
## errno:  2
## filename:  nawak
## strerror:  No such file or directory

Il est même possible de les lever à nouveau après :

try:
    f = open("nawak")
except IOError as e:
    print("args: ", e.args)
    print("errno: ", e.errno)
    print("filename: ", e.filename)
    print("strerror: ", e.strerror)
    raise e

raise appelé sans argument lève de toute façon automatiquement l’exception en cours. Mais il est possible de lever une autre exception :

try:
    f = open("nawak")
except IOError as e:
    print("args: ", e.args)
    print("errno: ", e.errno)
    print("filename: ", e.filename)
    print("strerror: ", e.strerror)
    raise MonError("T'as encore oublie ton classeur Man")

Erreur à la sortie de l’interpréteur

Il est possible d’attraper n’importe quelle exception non gérée juste avant qu’elle fasse crasher le programme, sans avoir à mettre tout son code dans un gros try/except.

Il faut définir une fonction qui accepte 3 arguments que sont la classe de l’exception, l’instance de l’exception et l’objet traceback :

def attrapez_les_tous(type, value, traceback):
    print("Pokemon !")

Ensuite il faut l’attacher au module sys avec le bon nom :

import sys
sys.excepthook = attrapez_les_tous

Et hop :

1 / 0
Pokemon !

Alors, c’est certain que si vous faites ça, vous avez intérêt à savoir ce que vous branlez car vous pouver tuer le debuggage. Ou alors créer un super moyen de logger toute erreur sur un process séparé qui vous le présente dans une belle interface Web. Au choix.

Les exceptions en dehors des erreurs

Les exceptions sont la pour gérer des cas exceptionnels, et pas forcément une erreur. Par exemple, la boucle for utilise les exceptions pour savoir quand s’arrêter. En effet elle attend que l’itérateur qu’elle utilise lève StopIteration :

>>> iterateur = iter(range(3))
>>> next(iterateur)
0
>>> next(iterateur)
1
>>> next(iterateur)
2
>>> next(iterateur)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
 in ()
----> 1 next(iterateur)

StopIteration:

Donc quand on fait ça :

for x in range(3):
    print(x)

En fait, la boucle fait un truc comme ça :

iterateur = iter(range(3))
while True:
    try:
        print(next(iterateur))
    except StopIteration:
        break

Les exceptions en Python, c’est une grande histoire d’amour.

]]>
http://sametmax.com/gestion-des-erreurs-en-python/feed/ 22 15786