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…
Lorsque tu dis “En Python, les erreurs (et même plus) […]” Tu peux lier ça à ces articles pour illustrer : http://sametmax.com/dis-papa-dis-papa-dis-moi-dis-moi-comment-cest-fait-dans-une-boucle-for/ et http://sametmax.com/sortir-de-plusieurs-boucles-for-imbriquees-en-python/
Deux articles (en anglais) pour enfoncer le clou sur les exceptions!
http://stupidpythonideas.blogspot.fr/2015/05/if-you-dont-like-exceptions-you-dont.html
http://stupidpythonideas.blogspot.com/2015/05/how-to-detect-valid-integer-literal.html
Mouais. Le mec aurait pu au-moins faire un
return None
dans son except. Le test sur l’appel aurait été le même mais ça évitait ce tuple vide, inutile consommateur de ressources et peu représentatif.Même si je connais bien (et que j’utilise) les exceptions, j’ai beaucoup aimé cet article (comme tous les autres d’ailleurs). Petite coquille:
Grave à finallyGrâce à finally…Typo:
la progragation de l’erreur => propagation
que je n’avais pas prévu => prévue (une erreur)
soit tu te sers par du truc => pas
sur tout système = > sur tous systèmes
Sinon, très bon article, comme d’habitude.
Je connaissais pas le terme de exception bubble, intéressant.
@Fred: un
return
tout court fait la même chose qu’unreturn None
, de même qu’une méthode de classe qui ne fait pas de return explicite retournera un None aussi par défaut.Par rapport à la réponse simple, courte et définitive :
je ne comprends pas bien la nuance qui est faite avec les langages utilisant les codes de retour comme C : Python dispose aussi du return, non ?
Par exemple, dans une fonction qui recherche un élément dans un ensemble, est-ce qu’un
return None
n’est pas suffisant pour signaler l’erreur ?Autre exemple, lors de la connexion à un serveur, un
return False
peut-il convenir pour signaler l’échec de l’ouverture de connexion ?@toub:
Ce n’est pas une question de capacité, c’est une question de style en vigueur. Tu peux recoder un système d’exceptions en C et l’utiliser. Tu peux retourner des codes d’erreur en Python. Néanmoins, en C, le style que la communauté utilise est majoritairement l’utilisation du code d’erreur. Et en Python, le style utilisé majoritaire par la communauté est l’usage d’exceptions. De fait, les collègues s’attendent à cela. Les outils s’attendent à cela. Les docs s’attendent à cela.
Ne pas retrouver un élément n’est pas une erreur. On retourne None, non pas pour signaler une erreur, mais pour retourner la valeur qui a le plus de sens dans ce contexte. Ici, on ne trouve rien. Comme None représente la notin de “rien”, c’est ce qu’on utilise.
Oui, c’est possible. Mais encore une fois ce n’est pas une question de capacité, c’est une question de style. Ce n’est pas ce qu’on fera en Python. Stylistiquement, un échec de connexion se fera avec une exception.
En l’occurence, ont en obtient les bénéfices suivant:
@François, @sPaz, @mentat, @Fred: Merci !
@toub @Fred
Le fait d’utiliser un tuple vide ici est beaucoup plus propre à l’utilisation car il évite d’avoir à tester à chaque site d’appel pour vérifier que le résultat n’est pas None (ce qui cause tant d’erreurs en C) avant de le passer à une boucle for par exemple: un tuple même vide est un itérable. De plus c’est conceptuellement plus clair: la fonction retourne l’ensemble des valeurs correspondant à la recherche, pas une fois un ensemble et une fois un autre truc.
@sam @cym13
OK ça y est je vois la nuance, merci ! (je comprends vite mais faut m’expliquer longtemps)
Bien vu cym13 le coup du tuple pour un retour de résultat vide, qui évite à gérer le cas du None, comme tu dis grosse source d’erreur en C.
Question subsidiaire : y a-t-il une siouxerie python pour gérer le nettoyage dans la fonction qui lève l’exception ? ou bien faut-il gérer à la mimine le nettoyage avant d’invoquer le
raise
(attention, je ne parle pas du nettoyage dans lefinally
qui nettoie au moment du catch de l’exception)Il faut que tu me montres du code car là réponse va vraiment dépendre ce celui-ci.
Comme pour le moment mon code n’a pas le moindre début d’usage d’exception (ce que je souhaite corriger) je n’ai pas de vrai code à te donner.
Le cas d’usage c’est un truc de ce gout la:
Comment gérer l’arrêt du thread, la fermeture de la socket… de maniere unifiee pour les 2 raise d’exception ?
Bon l’exemple est rapide et pourri de bugs potentiels, mais ce qui m’intéressesse en l’occurence c’est surtout le nettoyage des données suite aux
raise
@toub
Au pire amène toi sur indexerror pour cette question, le format s’y prête beaucoup plus, de même que la qualité des réponses.
On pourra en discuter tranquillement histoire de laisser la place ici pour les com’s un peu moins précis/techniques :)
+1. Surtout que la réponse risque d’être longue.
De manière générale, une bonne manière de gérer les erreurs est de le faire en mode “Fail Fast”.
Le Fail Fast consiste à ne pas traiter des situations qui ne devraient pas arriver. Par exemple, il ne faut pas tester à null à chaque fois qu’un objet est passé en paramètre. Ou il ne faut pas vérifier si un entier est toujours positif lorsqu’on passe un index de tableau.
Il faut au contraire ne rien faire. Ca plantera si un null ou un entier négatif arrive. Mais si ça plante, ce n’est pas le code de traitement du null ou de l’entier qui bug, c’est avant. Un mauvais paramètre aura été passé et il faut en trouver la raison.
Le fail fast permet de ne pas surcharger le code avec de la verbosité qui ne fera que rendre la correction de bug plus complexe encore.
Si je me trompe pas, si les exceptions sont “rapides” en python, c’est que les autres opérations sont lentes donc relativement, les exceptions sont plutôt rapides.
Ce que je veux dire, c’est que python n’a pas trouvé de recette magique pour que les exceptions deviennent miraculeusement rapide dans l’absolu.
@toub :
Par rapport au premier exemple, j’aurais tendance à le faire de 2 manières différentes en fonction du contexte :
Si j’ai absolument besoin d’un (et un seul) retour, ma fonction sera
get_element()
et lèvera une exception si l’element n’existe pas. Sinon, c’est seulement une recherche qui peut ne rien trouver, et la fonction serafind_element()
ousearch_element()
, et ça retourneraNone
si je cherche un seul element, ou un iterable vide si je peux avoir plusieurs elements (find_elements()
). Dans ce cas, c’est la fonction appelante qui se chargera de lever une exception si l’absence de retour est finalement problématique.@Sam
Ce que t’expliques pour les exceptions qui sont au coeur de python, marche aussi pour le logging qui y est intégré.
Écrire print() pour loguer en python c’est contre l’esprit du langage.
En fait, quand tu débarques dans un langage, il y a tous les non dits et les usages de la “communauté” qui ne sont pas explicites.
Donc non seulement faut apprendre le langage mais aussi les bonnes pratiques de la communauté.
C’est tout l’intérêt de ce blog qui explique bien l’esprit du langage.
J’ai réécrit une bonne partie de mon code python après la lecture de ce blog.
Merci pour l’article, interessant comme d’habitude !
Et deux petites coquilles :
Parce que c’est la manière de faire
quede la communautéelles sont au coeur du fonctionnement du langage, et pas juste pour
pourles erreurs.Merci !