Vérifier première vs gestion des exceptions?


88

Je travaille sur le livre "Head First Python" (c'est ma langue à apprendre cette année) et je suis arrivé dans une section où ils discutent de deux techniques de code:
Checking First vs Exception traitant.

Voici un exemple du code Python:

# Checking First
for eachLine in open("../../data/sketch.txt"):
    if eachLine.find(":") != -1:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

# Exception handling        
for eachLine in open("../../data/sketch.txt"):
    try:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    except:
        pass

Le premier exemple traite directement d'un problème dans la .splitfonction. Le second laisse simplement le gestionnaire d'exceptions le gérer (et ignore le problème).

Ils expliquent dans le livre d'utiliser la gestion des exceptions au lieu de vérifier en premier. L'argument est que le code d'exception interceptera toutes les erreurs, alors que vérifier en premier ne capturera que les choses auxquelles vous pensez (et vous manquez les cas critiques). On m'a appris à vérifier d'abord, alors mon instinct de départ était de le faire, mais leur idée est intéressante. Je n'avais jamais pensé à utiliser le traitement des exceptions pour traiter les cas.

Lequel des deux est généralement considéré comme la meilleure pratique?


12
Cette section du livre n'est pas intelligente. Si vous êtes dans une boucle et que vous lancez des exceptions à plusieurs reprises, cela coûte très cher. J'ai essayé de souligner quelques points positifs pour savoir quand faire cela.
Jason Sebring

9
Il suffit de ne pas tomber dans le piège "fichier existe vérifier". Le fichier existe! = A accès au fichier ou existera dans les 10 ms nécessaires pour accéder à mon fichier, appel ouvert, etc. blogs.msdn.com/b/jaredpar/archive/2009/04/27/…
Billy ONeal

11
Les exceptions sont pensées différemment en Python par rapport aux autres langages. Par exemple, pour parcourir une collection, il suffit d'appeler .next () jusqu'à ce qu'il lève une exception.
WuHoUnited

4
@ emeraldcode.com Ce n'est pas tout à fait vrai à propos de Python. Je ne connais pas les détails, mais le langage a été construit autour de ce paradigme, de sorte que le lancement d'exceptions est moins coûteux que dans d'autres langages.
Izkata

Cela dit, pour cet exemple, j'utiliserais une déclaration de protection:, if -1 == eachLine.find(":"): continuele reste de la boucle ne serait pas non plus en retrait.
Izkata

Réponses:


68

Dans .NET, il est courant d'éviter la surutilisation des exceptions. L'un des arguments est la performance: dans .NET, le lancement d'une exception est coûteux en calcul.

Une autre raison d'éviter leur utilisation abusive est qu'il peut être très difficile de lire du code qui repose trop sur eux. L' entrée de blog de Joel Spolsky décrit bien le problème.

Au cœur de l'argumentation se trouve la citation suivante:

Le raisonnement est que je considère que les exceptions ne valent pas mieux que les "goto", considérées comme nuisibles depuis les années 1960, en ce qu'elles créent un saut brutal d'un point de code à un autre. En fait, ils sont nettement pires que ceux que nous connaissons

1. Ils sont invisibles dans le code source . En regardant un bloc de code, y compris des fonctions qui peuvent ou non renvoyer des exceptions, il n'y a aucun moyen de voir quelles exceptions peuvent être levées et d'où. Cela signifie que même une inspection minutieuse du code ne révèle pas de bugs potentiels.

2. Ils créent trop de points de sortie possibles pour une fonction. Pour écrire du code correct, vous devez vraiment penser à tous les chemins de code possibles dans votre fonction. Chaque fois que vous appelez une fonction qui peut générer une exception et ne l'attrape pas sur-le-champ, vous créez des opportunités pour des bugs inattendus provoqués par des fonctions qui se sont terminées abruptement, laissant ainsi les données incohérentes ou d'autres chemins de code que vous n'avez pas connus. Penser à.

Personnellement, je jette des exceptions lorsque mon code ne peut pas faire ce pour quoi il est sous contrat. J'ai tendance à utiliser try / catch lorsque je suis sur le point de traiter quelque chose en dehors de la limite de mon processus, par exemple un appel SOAP, un appel à une base de données, un fichier IO ou un appel système. Sinon, je tente de coder de manière défensive. Ce n'est pas une règle absolue, mais c'est une pratique générale.

Scott Hanselman écrit également sur les exceptions dans .NET ici . Dans cet article, il décrit plusieurs règles empiriques concernant les exceptions. Mon favori?

Vous ne devriez pas créer d'exceptions pour les choses qui arrivent tout le temps. Ensuite, ils seraient des "ordinaires".


5
autre point: si la journalisation des exceptions est activée au niveau de l’application, il est préférable d’utiliser exception uniquement pour des conditions exceptionnelles, et non pour des opérations ordinaires. Sinon, le journal deviendra encombré et les véritables causes d'erreur seront masquées.
Rwong

2
Bonne réponse. Notez que les exceptions ont des performances élevées sur la plupart des plateformes. Cependant, comme vous l’auriez noté avec mes commentaires sur d’autres réponses, la performance n’est pas prise en compte dans le cas de la décision d’une règle générale sur la manière de codifier quelque chose.
mattnz

1
La citation de Scott Hanselman décrit mieux l’attitude de .Net à l’égard des exceptions que la "surutilisation". Les performances sont souvent mentionnées, mais le véritable argument est l'inverse de la raison pour laquelle vous DEVRIEZ utiliser des exceptions - cela rend le code plus difficile à comprendre et à traiter lorsqu'une condition ordinaire entraîne une exception. En ce qui concerne Joel, le point 1 est en fait un point positif (invisible signifie que le code indique ce qu’il fait et non ce qu’il ne fait pas), et le point 2 est sans importance (vous êtes déjà dans un état incohérent ou vous ne devriez pas faire exception à la règle). . Toujours, +1 pour "ne peut pas faire ce qu'on lui a demandé de faire".
Jmoreno

5
Bien que cette réponse convienne à .Net, elle n’est pas très pythonique . C’est donc une question de python. Je ne vois donc pas pourquoi la réponse d’ Ivc n’a pas été votée davantage.
Mark Booth

2
@ IanGoldby: non. La gestion des exceptions est en réalité mieux décrite comme une récupération d’exception. Si vous ne pouvez pas récupérer d'une exception, vous ne devriez probablement pas avoir de code de gestion des exceptions. Si la méthode A appelle la méthode B qui appelle C, et C renvoie, alors le plus probable est que SO OU A ou B devrait récupérer, pas les deux. La décision "si je ne peux pas faire X je ferai Y" devrait être évitée si Y a besoin de quelqu'un d'autre pour terminer la tâche. Si vous ne pouvez pas terminer la tâche, il ne reste que le nettoyage et la journalisation. Le nettoyage dans .net doit être automatique, la journalisation doit être centralisée.
Jmoreno

78

En Python en particulier, il est généralement considéré comme une meilleure pratique d'attraper l'exception. Il a tendance à s'appeler plus facile à demander pardon que la permission (EAFP), comparé à Look Before You Leap (LBYL). Il y a des cas où LBYL vous donnera des bugs subtils dans certains cas.

Cependant, faites attention aux except:déclarations nues et aux déclarations trop générales, car elles peuvent toutes les deux masquer des bogues - une telle chose serait mieux:

for eachLine in open("../../data/sketch.txt"):
    try:
        role, lineSpoken = eachLine.split(":",1)
    except ValueError:
        pass
    else:
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

8
En tant que programmeur .NET, je grince des dents. Mais encore une fois, vous faites tout ce qui est bizarre. :)
Phil

Ceci est exceptionnellement frustrant (jeu de mots non prévu) lorsque les API ne sont pas cohérentes sur les exceptions lancées dans quelles circonstances, ou lorsque plusieurs types d'échecs différents sont générés sous le même type d'exception.
Jack

Vous finissez donc par utiliser le même mécanisme pour les erreurs inattendues et les types de retour attendus. Cela équivaut à utiliser 0 comme nombre, un faux bool ET un pointeur invalide qui quittera votre processus avec un code de sortie de 128 + SIGSEGV, car il est pratique de ne pas avoir besoin de choses différentes maintenant. Comme le spork! Ou des chaussures avec des orteils ...
yeoman

2
@yeoman quand lancer une exception est une question différente, celle-ci concerne l'utilisation de try/ exceptplutôt que la création d'une condition pour "est le suivant susceptible de lever une exception", et la pratique de Python est clairement de préférer l'ancienne. Cela ne fait pas de mal que cette approche soit (probablement) plus efficace ici, car, dans le cas où la scission est réussie, vous ne faites marcher la corde qu'une seule fois. Pour ce qui splitest de savoir si une exception devrait être jetée ici, je dirais que c’est absolument le cas - une règle commune est que vous devriez jeter lorsque vous ne pouvez pas faire ce que dit votre nom et que vous ne pouvez pas vous séparer d’un séparateur manquant.
LV

Je ne le trouve pas mauvais ou lent ou terrible, d'autant plus que seule une exception spécifique est capturée. Ans j'aime vraiment Python. C'est marrant de constater à quel point cela ne montre parfois aucun goût, comme l'a dit C, l'utilisation du chiffre zéro, le Spork et les chaussures préférées de Randall Munroe avec les orteils :) De plus, quand je suis en Python et qu'une API dit que c'est la façon de le faire, je vais y aller :) Vérifier les conditions à l’avance n’est bien sûr jamais une bonne idée en raison de la simultanéité des accès, des coroutines ou de l’ajout de ceux-ci en bas de la route ...
yeoman

27

Une approche pragmatique

Vous devriez être sur la défensive mais jusqu'à un certain point. Vous devriez écrire la gestion des exceptions mais jusqu'à un certain point. Je vais utiliser la programmation Web à titre d'exemple car c'est là que je vis.

  1. Supposons que toutes les entrées utilisateur sont incorrectes et n'écrivent de manière défensive qu'au point de vérification du type de données, de vérification des modèles et d'injection malveillante. La programmation défensive devrait être une chose qui peut potentiellement arriver très souvent et que vous ne pouvez pas contrôler.
  2. Écrivez la gestion des exceptions pour les services en réseau qui peuvent parfois échouer et gérez-la correctement pour les commentaires des utilisateurs. La programmation d'exceptions doit être utilisée pour les choses en réseau qui peuvent échouer de temps en temps mais sont généralement solides ET vous devez garder votre programme en marche.
  3. Ne vous donnez pas la peine d'écrire de manière défensive dans votre application après la validation des données d'entrée. C'est une perte de temps et gonfle votre application. Laissez-le exploser parce que c'est soit quelque chose de très rare qui ne vaut pas la peine d'être manipulé ou cela signifie que vous devez examiner les étapes 1 et 2 plus attentivement.
  4. N'écrivez jamais dans votre code principal une gestion des exceptions qui ne dépend pas d'un périphérique en réseau. Cela est une mauvaise programmation et une performance coûteuse. Par exemple, écrire un try-catch en cas de tableau hors limites dans une boucle signifie que vous n’avez pas programmé la boucle correctement.
  5. Laissez tout être géré par la consignation centralisée des erreurs qui intercepte les exceptions en un seul endroit après avoir suivi les procédures ci-dessus. Vous ne pouvez pas capturer tous les cas car cela peut être infini, il vous suffit d'écrire du code qui gère les opérations attendues. C'est pourquoi vous utilisez le traitement central des erreurs en dernier recours.
  6. TDD est agréable parce qu’en un sens, essayer d’attraper pour vous sans bouffonner, ce qui signifie vous donner une certaine assurance du fonctionnement normal.
  7. Les points bonus consistent à utiliser un outil de couverture de code, par exemple, Istanbul est un bon outil pour les nœuds, car il vous indique où vous ne testez pas.
  8. La mise en garde à tout cela est des exceptions favorables aux développeurs . Par exemple, un langage jetterait si vous utilisiez la syntaxe incorrecte et expliquez pourquoi. Il convient donc que vos bibliothèques d’ utilitaires dépendent de la majeure partie de votre code.

Cela provient de l'expérience de travail dans des scénarios de grande équipe.

Une analogie

Imaginez si vous portiez une combinaison spatiale à l'intérieur de l'ISS TOUT le temps. Il serait difficile d'aller aux toilettes ou de manger, du tout. Ce serait super encombrant de se déplacer dans le module spatial. Ce serait nul. Écrire un tas d'essais dans votre code est un peu comme ça. Vous devez avoir un moment où vous dites, hé, j’ai sécurisé l’ISS et que mes astronautes à l’intérieur sont en bon état, il n’est donc pas pratique de porter une combinaison spatiale pour chaque scénario qui pourrait éventuellement se produire.


4
Le problème avec le point 3 est qu’il suppose que le programme et les programmeurs qui y travaillent sont parfaits. Ils ne le sont pas, il est donc préférable de programmer en tenant compte de ces objectifs. Des montants appropriés au moment clé peuvent rendre le logiciel beaucoup plus fiable que la mentalité "Si les entrées sont vérifiées parfaitement".
mattnz

c'est à cela que servent les tests.
Jason Sebring

3
Les tests ne sont pas un piège à tous. Je n'ai pas encore vu de suite de tests contenant 100% de code et une couverture "environnementale".
Marjan Venema

1
@emeraldcode: Voulez-vous un emploi avec moi? J'aimerais beaucoup de quelqu'un dans l'équipe qui testera toujours, à l'exception des tests, toutes les permutations de tous les cas où le logiciel sera exécuté. Doit être agréable de savoir avec la certitude absolue que votre code est parfaitement testé.
mattnz

1
Se mettre d'accord. Il existe des scénarios dans lesquels la programmation défensive et la gestion des exceptions fonctionnent correctement et négativement, et nous, programmeurs, devrions apprendre à les reconnaître et à choisir la technique qui convient le mieux. J'aime le point 3 car je pense que nous devons supposer, à un certain niveau du code, que certaines conditions contextuelles doivent être satisfaites. Ces conditions sont satisfaites en codant de manière défensive dans la couche externe de code, et je pense que la gestion des exceptions convient parfaitement lorsque ces hypothèses sont rompues dans la couche interne.
yaobin

15

L'argument principal du livre est que la version d'exception du code est préférable car elle intercepte tout ce que vous pourriez avoir oublié si vous tentiez d'écrire votre propre vérification d'erreur.

Je pense que cette affirmation n’est vraie que dans des circonstances très spécifiques - où vous ne vous souciez pas de savoir si le résultat est correct.

Il ne fait aucun doute que le fait de lever des exceptions est une pratique saine et sûre. Vous devriez le faire chaque fois que vous sentez qu'il y a quelque chose dans l'état actuel du programme que vous (en tant que développeur) ne pouvez pas, ou ne voulez pas, traiter.

Votre exemple, cependant, concerne la capture d’ exceptions. Si vous attrapez une exception, vous ne vous protégez pas des scénarios que vous avez peut-être négligés. Vous faites exactement le contraire: vous supposez que vous n’avez négligé aucun scénario qui aurait pu causer ce type d’exception et vous êtes donc confiant que vous pouvez l’attraper (et l’empêcher ainsi de provoquer la fermeture du programme, comme le ferait toute exception non interceptée).

En utilisant l'approche d'exception, si vous voyez une ValueErrorexception, vous sautez une ligne. En utilisant l'approche traditionnelle sans exception, vous comptez le nombre de valeurs renvoyées splitet, s'il est inférieur à 2, vous sautez une ligne. Devriez-vous vous sentir plus en sécurité avec l'approche d'exception, puisque vous auriez peut-être oublié d'autres situations "d'erreur" dans votre vérification d'erreur classique et que vous except ValueErrorvoudriez les résoudre?

Cela dépend de la nature de votre programme.

Si vous écrivez, par exemple, dans un navigateur Web ou un lecteur vidéo, un problème d’entrées ne devrait pas provoquer sa panne avec une exception non interceptée. Il est de loin préférable de sortir quelque chose de sensible (même si, à proprement parler, d’erreur) que de quitter.

Si vous écrivez une application dans laquelle l'exactitude est importante (comme un logiciel commercial ou d'ingénierie), ce serait une approche terrible. Si vous avez oublié certains scénarios ValueError, la pire chose à faire est d'ignorer en silence ce scénario inconnu et de simplement sauter la ligne. C'est ainsi que des bogues très subtils et coûteux se retrouvent dans les logiciels.

Vous pourriez penser que la seule façon de voir ValueErrorce code est de splitrenvoyer une seule valeur (au lieu de deux). Mais que se passe-t-il si, par la suite, votre printdéclaration commence à utiliser une expression qui soulève ValueErrorcertaines conditions? Cela vous fera sauter certaines lignes non pas parce qu'elles manquent :, mais parce printqu'elles échouent. Voici un exemple de bogue subtil auquel je faisais référence précédemment: vous ne remarqueriez rien, vous perdez simplement quelques lignes.

Ma recommandation est d'éviter d'attraper (mais pas de lever!) Des exceptions dans le code où produire une sortie incorrecte est pire que sortir. La seule fois où j'attrape une exception dans un tel code, c'est lorsque j'ai une expression vraiment triviale. Je peux donc facilement déterminer la cause de chacun des types d'exception possibles.

En ce qui concerne l'impact sur les performances de l'utilisation des exceptions, il est trivial (en Python), à moins que des exceptions ne soient rencontrées fréquemment.

Si vous utilisez des exceptions pour gérer des conditions courantes, vous pouvez parfois payer un coût de performances énorme. Par exemple, supposons que vous exécutiez une commande à distance. Vous pouvez vérifier que le texte de votre commande passe au moins la validation minimale (par exemple, la syntaxe). Vous pouvez également attendre qu'une exception soit déclenchée (ce qui ne se produit qu'une fois que le serveur distant a analysé votre commande et a constaté un problème). De toute évidence, le premier est beaucoup plus rapide. Autre exemple simple: vous pouvez vérifier si un nombre est égal à zéro ~ 10 fois plus rapidement que d'essayer d'exécuter la division puis d'attraper l'exception ZeroDivisionError.

Ces considérations importent uniquement si vous envoyez fréquemment des chaînes de commandes mal formées à des serveurs distants ou si vous recevez des arguments de valeur zéro que vous utilisez pour la division.

Note: Je suppose que vous utiliseriez à la except ValueErrorplace du juste except; comme d'autres l'ont fait remarquer, et comme le livre lui-même l'indique en quelques pages, il ne faut jamais utiliser nu except.

Autre remarque: l'approche appropriée sans exception consiste à compter le nombre de valeurs renvoyées par splitplutôt qu'à la recherche :. Ce dernier est beaucoup trop lent, car il répète le travail effectué splitet peut presque doubler le temps d'exécution.


6

En règle générale, si vous savez qu'une instruction peut générer un résultat non valide, testez-la et corrigez-la. Utilisez des exceptions pour des choses que vous n'attendez pas; des trucs qui sont "exceptionnels". Cela rend le code plus clair dans un sens contractuel ("ne devrait pas être nul" à titre d'exemple).


2

Utilisez ce qui marche bien dans ..

  • votre langage de programmation choisi en termes de lisibilité et d'efficacité du code
  • votre équipe et l'ensemble des conventions de code convenues

La gestion des exceptions et la programmation défensive sont des manières différentes d'exprimer la même intention.


0

TBH, peu importe que vous utilisiez le try/exceptmécanicien ou un ifcontrôle de relevé. Vous voyez généralement à la fois EAFP et LBYL dans la plupart des lignes de base Python, EAFP étant légèrement plus commun. Parfois, EAFP est beaucoup plus lisible / idiomatique, mais dans ce cas particulier, je pense que ça va de toute façon.

Pourtant...

Je ferais attention en utilisant votre référence actuelle. Quelques problèmes criants avec leur code:

  1. Le descripteur de fichier est divulgué. Les versions modernes de CPython (un interpréteur Python spécifique ) le fermeront, puisqu'il s'agit d'un objet anonyme dont l'étendue est limitée pendant la boucle (gc le supprimera après la boucle). Cependant, d'autres interprètes n'ont pas cette garantie. Ils risquent de laisser échapper le descripteur. Vous voulez presque toujours utiliser l' withidiome lors de la lecture de fichiers en Python: il y a très peu d'exceptions. Ce n'est pas l'un d'eux.
  2. La gestion des exceptions Pokemon est mal vue car elle masque les erreurs (c.-à-d. Des exceptinstructions nues qui ne détectent aucune exception spécifique)
  3. Nit: Vous n'avez pas besoin de parens pour décompresser les tuples. Peut juste fairerole, lineSpoken = eachLine.split(":",1)

Ivc a une bonne réponse à ce sujet et à l'EAFP, mais fait également fuir le descripteur.

La version LBYL n'étant pas nécessairement aussi performante que la version EAFP, affirmer que le lancement d'exceptions est "coûteux en termes de performances" est catégoriquement faux. Cela dépend vraiment du type de chaîne que vous traitez:

In [33]: def lbyl(lines):
    ...:     for line in lines:
    ...:         if line.find(":") != -1:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = line.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:

In [34]: def eafp(lines):
    ...:     for line in lines:
    ...:         try:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = eachLine.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:         except:
    ...:             pass
    ...:

In [35]: lines = ["abc:def", "onetwothree", "xyz:hij"]

In [36]: %timeit lbyl(lines)
100000 loops, best of 3: 1.96 µs per loop

In [37]: %timeit eafp(lines)
100000 loops, best of 3: 4.02 µs per loop

In [38]: lines = ["a"*100000 + ":" + "b", "onetwothree", "abconetwothree"*100]

In [39]: %timeit lbyl(lines)
10000 loops, best of 3: 119 µs per loop

In [40]: %timeit eafp(lines)
100000 loops, best of 3: 4.2 µs per loop

-4

Fondamentalement, la gestion des exceptions est censée être plus appropriée pour les langues POO.

Le deuxième point est la performance, car vous n'avez pas à exécuter eachLine.findpour chaque ligne.


7
-1: Les performances sont une raison extrêmement mauvaise pour les règles générales.
mattnz

3
Non, les exceptions ne sont absolument pas liées à la POO.
Pubby

-6

Je pense que la programmation défensive nuit aux performances. Vous devez également ne capturer que les exceptions que vous allez gérer, laissez le runtime s'occuper de l'exception que vous ne savez pas gérer.


7
Encore une anotehr -1 pour s'inquiéter de la performance sur la lisibilité, la maintenabilité bla bla bla. La performance n'est pas une raison.
mattnz

Puis-je savoir pourquoi vous distribuez des -1 sans expliquer? La programmation défensive signifie plus de lignes de code, ce qui signifie de moins bonnes performances. Quelqu'un veut-il expliquer avant d'abattre le score?
Manoj

3
@Manoj: Sauf si vous avez mesuré avec un profileur et trouvé un bloc de code trop lent, codez pour la lisibilité et la maintenabilité bien avant les performances.
Daenyth

Ce que @Manoj a dit avec l’ajout que moins de code signifie universellement moins de travail lors du débogage et de la maintenance. Le temps passé par les développeurs pour un code moins parfait est extrêmement élevé. Je suppose (comme moi) que vous n'écrivez pas un code parfait, pardonnez-moi si je me trompe.
mattnz

2
Merci pour le lien - Intéressant lu que je dois être d’accord avec ça, jusqu’à un point ... Travailler sur un système essentiel, comme je le fais "Le système imprimait la trace de la pile, nous savons donc exactement pourquoi ces 300 personnes sont mortes inutilement. .... "ne va pas vraiment aller trop mal à la barre des témoins. Je suppose que c'est une de ces choses où chaque situation a une réponse appropriée différente.
mattnz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.