Utiliser try vs if en python

145

Y a-t-il une justification pour décider lequel des tryou des ifconstructions à utiliser lors du test d'une variable pour avoir une valeur?

Par exemple, il existe une fonction qui renvoie une liste ou ne renvoie pas de valeur. Je veux vérifier le résultat avant de le traiter. Laquelle des propositions suivantes serait la plus préférable et pourquoi?

result = function();
if (result):
    for r in result:
        #process items

ou

result = function();
try:
    for r in result:
        #process items
except TypeError:
    pass;

Discussion connexe:

Vérification de l'existence d'un membre dans Python

artdanil
la source
Gardez à l'esprit que si votre strophe #process items est susceptible de générer une erreur TypeError, vous devez encapsuler dans un autre try: except: block. Pour cet exemple spécifique, j'utiliserais simplement un if:
richo

Réponses:

237

Vous entendez souvent que Python encourage le style EAFP ("il est plus facile de demander pardon que la permission") par rapport au style LBYL ("regardez avant de sauter"). Pour moi, c'est une question d'efficacité et de lisibilité.

Dans votre exemple (disons qu'au lieu de renvoyer une liste ou une chaîne vide, la fonction devait renvoyer une liste ou None), si vous vous attendez à ce que 99% du temps resultcontienne réellement quelque chose d'itérable, j'utiliserais l' try/exceptapproche. Ce sera plus rapide si les exceptions sont vraiment exceptionnelles. Si resultc'est Noneplus de 50% du temps, l'utilisation ifest probablement meilleure.

Pour soutenir cela avec quelques mesures:

>>> import timeit
>>> timeit.timeit(setup="a=1;b=1", stmt="a/b") # no error checking
0.06379691968322732
>>> timeit.timeit(setup="a=1;b=1", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.0829463709378615
>>> timeit.timeit(setup="a=1;b=0", stmt="try:\n a/b\nexcept ZeroDivisionError:\n pass")
0.5070195056614466
>>> timeit.timeit(setup="a=1;b=1", stmt="if b!=0:\n a/b")
0.11940114974277094
>>> timeit.timeit(setup="a=1;b=0", stmt="if b!=0:\n a/b")
0.051202772912802175

Ainsi, alors qu'une ifdéclaration vous coûte toujours , il est presque gratuit de mettre en place un try/exceptblocage. Mais lorsqu'un Exceptionproblème survient réellement, le coût est beaucoup plus élevé.

Moral:

  • C'est parfaitement OK (et "pythonique") à utiliser try/exceptpour le contrôle de flux,
  • mais cela a plus de sens quand les Exceptions sont réellement exceptionnels.

À partir de la documentation Python:

EAFP

Plus facile de demander pardon que la permission. Ce style de codage Python commun suppose l'existence de clés ou d'attributs valides et intercepte les exceptions si l'hypothèse s'avère fausse. Ce style propre et rapide se caractérise par la présence d' un grand nombre tryet exceptdéclarations. La technique contraste avec le style LBYL commun à de nombreux autres langages tels que C.

Tim Pietzcker
la source
1
Je vous remercie. Je vois que dans ce cas, la justification peut être l'attente du résultat.
artdanil
6
.... et c'est l'une (des nombreuses) raisons pour lesquelles il est si difficile de faire un vrai JIT d'optimisation pour Python. Comme le prouve récemment LuaJIT 2 en version bêta, les langages dynamiques peuvent être vraiment très rapides; mais cela dépend fortement de la conception initiale du langage et du style qu'il encourage. (sur une note connexe, la conception du langage est la raison pour laquelle même les meilleurs JIT JavaScirpt ne peuvent pas être comparés à LuaJIT 1, et encore moins à 2)
Javier
1
@ 2rs2ts: Je viens de faire des horaires similaires moi-même. En Python 3, try/exceptétait 25% plus rapide que if key in d:pour les cas où la clé était dans le dictionnaire. C'était beaucoup plus lent lorsque la clé n'était pas dans le dictionnaire, comme prévu, et cohérente avec cette réponse.
Tim Pietzcker
5
Je sais que c'est une vieille réponse, cependant: utiliser des déclarations comme 1/1avec timeit n'est pas un excellent choix, car elles seront optimisées (voyez dis.dis('1/1')et notez qu'aucune division ne se produit).
Andrea Corbellini
1
Cela dépend également de l'opération que vous êtes prêt à effectuer. Une seule division est rapide, attraper une erreur coûte un peu cher. Mais même dans les cas où le coût de la gestion des exceptions est acceptable, réfléchissez soigneusement à ce que vous demandez à l'ordinateur de faire. Si l'opération elle-même est très chère quels que soient ses résultats et qu'il existe un moyen peu coûteux de savoir si le succès est possible: utilisez-la. Exemple: ne copiez pas la moitié d'un fichier juste pour vous rendre compte qu'il n'y a pas assez d'espace.
Bachsau
14

Votre fonction ne doit pas renvoyer de types mixtes (c.-à-d. Liste ou chaîne vide). Il doit renvoyer une liste de valeurs ou simplement une liste vide. Ensuite, vous n'avez pas besoin de tester quoi que ce soit, c'est-à-dire que votre code se réduit à:

for r in function():
    # process items
Brandon
la source
2
Je suis entièrement d'accord avec vous sur ce point; cependant, la fonction n'est pas la mienne et je ne fais que l'utiliser.
artdanil
2
@artdanil: Vous pouvez donc envelopper cette fonction dans une fonction que vous écrivez et qui fonctionne comme celle à laquelle Brandon Corfman pense.
quamrana
24
ce qui soulève la question: le wrapper devrait-il utiliser un if ou un essayer de gérer le cas non itérable?
jcdyer
@quamrana, c'est exactement ce que j'essaye de faire. Donc, comme @jcd l'a souligné, la question est de savoir comment je dois gérer la situation dans la fonction wrapper.
artdanil
12

Veuillez ignorer ma solution si le code que je fournis n'est pas évident à première vue et que vous devez lire l'explication après l'exemple de code.

Puis-je supposer que "aucune valeur renvoyée" signifie que la valeur de retour est None? Si oui, ou si "aucune valeur" est False du point de vue booléen, vous pouvez effectuer les opérations suivantes, car votre code traite essentiellement "aucune valeur" comme "ne pas itérer":

for r in function() or ():
    # process items

Si function()retourne quelque chose qui n'est pas True, vous itérez sur le tuple vide, c'est-à-dire que vous n'exécutez aucune itération. C'est essentiellement LBYL.

tzot
la source
4

Votre deuxième exemple est cassé - le code ne lèvera jamais d'exception TypeError puisque vous pouvez parcourir les chaînes et les listes. Itérer à travers une chaîne ou une liste vide est également valide - cela exécutera le corps de la boucle zéro fois.

Dave Kirby
la source
4

Laquelle des propositions suivantes serait la plus préférable et pourquoi?

Regardez avant de sauter est préférable dans ce cas. Avec l'approche d'exception, une TypeError pourrait se produire n'importe où dans le corps de votre boucle et elle serait capturée et jetée, ce qui n'est pas ce que vous voulez et rendra le débogage délicat.

(Je suis cependant d'accord avec Brandon Corfman: renvoyer None pour "aucun élément" au lieu d'une liste vide est cassé. C'est une habitude désagréable des codeurs Java qui ne devrait pas être vue en Python. Ou Java.)

bobince
la source
4

En général, j'ai l'impression que les exceptions devraient être réservées aux circonstances exceptionnelles. Si l' resulton s'attend à ce que le ne soit jamais vide (mais qu'il pourrait l'être, si, par exemple, un disque tombe en panne, etc.), la deuxième approche a du sens. Si, en revanche, un vide resultest parfaitement raisonnable dans des conditions normales, le tester avec une ifinstruction a plus de sens.

J'avais en tête le scénario (le plus courant):

# keep access counts for different files
file_counts={}
...
# got a filename somehow
if filename not in file_counts:
    file_counts[filename]=0
file_counts[filename]+=1

au lieu de l'équivalent:

...
try:
    file_counts[filename]+=1
except KeyError:
    file_counts[filename]=1
Managu
la source
Voici un exemple de la différence entre les approches que Tim Pietzcker mentionne: Le premier est LBYL, le second est EAFP
Managu
Juste une note rapide qui ++ne fonctionne pas en python, utilisez += 1plutôt.
tgray
3

bobince fait remarquer à bon escient que le fait d'envelopper le deuxième cas peut également attraper TypeErrors dans la boucle, ce qui n'est pas ce que vous voulez. Si vous voulez vraiment utiliser un essai, vous pouvez tester s'il est itérable avant la boucle

result = function();
try:
    it = iter(result)
except TypeError:
    pass
else:
    for r in it:
        #process items

Comme vous pouvez le voir, c'est plutôt moche. Je ne le suggère pas, mais il devrait être mentionné pour être complet.

AFoglie
la source
À mes yeux, rencontrer des erreurs de type intentionnellement est toujours un mauvais style de codage. Un développeur doit savoir à quoi s'attendre et être prêt à gérer ces valeurs sans exception. Chaque fois qu'une opération a fait ce qu'elle était censée faire (du point de vue du programmeur) et que les résultats attendus sont une liste ou None, vérifiez toujours le résultat avec is Noneou is not None. D'un autre côté, il est également mauvais de soulever des exceptions pour des résultats légitimes. Les exceptions sont pour les choses inattendues. Exemple: str.find()renvoie -1 si rien n'est trouvé, car la recherche elle-même s'est terminée sans erreur.
Bachsau
1

En ce qui concerne les performances, l'utilisation du bloc try pour du code qui ne déclenche normalement pas d'exceptions est plus rapide que l'utilisation de l'instruction if à chaque fois. Ainsi, la décision dépend de la probabilité de cas excessifs.

Grayger
la source
-5

En règle générale, vous ne devez jamais utiliser try / catch ou tout autre élément de gestion d'exceptions pour contrôler le flux. Même si l'itération en coulisse est contrôlée via la levée d' StopIterationexceptions, vous devriez toujours préférer votre premier extrait de code au second.

ilowe
la source
7
Cela est vrai en Java. Python embrasse assez fortement l'EAFP.
fengb
3
C'est encore une pratique étrange. D'après mon expérience, les cerveaux de la plupart des programmeurs sont câblés pour ignorer l'EAFP. Ils finissent par se demander pourquoi un certain chemin a été choisi. Cela étant dit, il y a un moment et un endroit pour enfreindre chaque règle.
ilowe