Les exceptions pour le contrôle de flux sont-elles les meilleures pratiques en Python?

15

Je lis "Learning Python" et j'ai rencontré ce qui suit:

Les exceptions définies par l'utilisateur peuvent également signaler des conditions sans erreur. Par exemple, une routine de recherche peut être codée pour déclencher une exception lorsqu'une correspondance est trouvée au lieu de renvoyer un indicateur d'état que l'appelant doit interpréter. Dans ce qui suit, le gestionnaire d'exceptions try / except / else effectue le travail d'un testeur de valeur de retour if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Parce que Python est typé dynamiquement et polymorphe au cœur, les exceptions, plutôt que les valeurs de retour sentinelles, sont le moyen généralement préféré pour signaler de telles conditions.

J'ai vu ce genre de chose discuté plusieurs fois sur divers forums et des références à Python utilisant StopIteration pour terminer les boucles, mais je ne trouve pas grand-chose dans les guides de style officiels (PEP 8 a une référence désinvolte aux exceptions pour le contrôle de flux) ou des déclarations de développeurs. Y a-t-il quelque chose d'officiel qui déclare que c'est la meilleure pratique pour Python?

Ceci ( Les exceptions en tant que flux de contrôle sont-elles considérées comme un contre-modèle sérieux? Si oui, pourquoi? ), Plusieurs commentateurs affirment également que ce style est Pythonic. Sur quoi est-ce basé?

TIA

JB
la source

Réponses:

25

Le consensus général «ne pas utiliser d'exceptions!» Vient principalement d'autres langues et même il est parfois dépassé.

  • En C ++, lever une exception est très coûteux en raison du «déroulement de la pile». Chaque déclaration de variable locale est comme une withinstruction en Python, et l'objet dans cette variable peut exécuter des destructeurs. Ces destructeurs sont exécutés lorsqu'une exception est levée, mais également lors du retour d'une fonction. Cet «idiome RAII» est une fonctionnalité de langage intégrale et est extrêmement important pour écrire du code robuste et correct - donc RAII contre les exceptions bon marché était un compromis que C ++ a décidé vers RAII.

  • Au début de C ++, beaucoup de code n'était pas écrit d'une manière sûre: à moins que vous n'utilisiez réellement RAII, il est facile de perdre de la mémoire et d'autres ressources. Donc, lever des exceptions rendrait ce code incorrect. Ce n'est plus raisonnable puisque même la bibliothèque standard C ++ utilise des exceptions: vous ne pouvez pas faire comme si les exceptions n'existaient pas. Cependant, les exceptions sont toujours un problème lors de la combinaison de code C avec C ++.

  • En Java, chaque exception a une trace de pile associée. La trace de pile est très précieuse lors du débogage des erreurs, mais est un effort inutile lorsque l'exception n'est jamais imprimée, par exemple parce qu'elle n'a été utilisée que pour le flux de contrôle.

Ainsi, dans ces langues, les exceptions sont «trop chères» pour être utilisées comme flux de contrôle. En Python, c'est moins un problème et les exceptions sont beaucoup moins chères. De plus, le langage Python souffre déjà d'une surcharge qui rend le coût des exceptions imperceptibles par rapport à d'autres constructions de flux de contrôle: par exemple, vérifier si une entrée dict existe avec un test d'appartenance explicite if key in the_dict: ...est généralement exactement aussi rapide que d'accéder simplement à l'entréethe_dict[key]; ... et vérifier si vous obtenir une KeyError. Certaines fonctionnalités du langage intégré (par exemple les générateurs) sont conçues en termes d'exceptions.

Ainsi, bien qu'il n'y ait aucune raison technique d'éviter spécifiquement les exceptions en Python, il reste la question de savoir si vous devez les utiliser à la place des valeurs de retour. Les problèmes de conception avec les exceptions sont les suivants:

  • ils ne sont pas du tout évidents. Vous ne pouvez pas facilement regarder une fonction et voir quelles exceptions elle peut lancer, donc vous ne savez pas toujours quoi attraper. La valeur de retour a tendance à être mieux définie.

  • les exceptions sont le flux de contrôle non local qui complique votre code. Lorsque vous lancez une exception, vous ne savez pas où le flux de contrôle reprendra. Pour les erreurs qui ne peuvent pas être traitées immédiatement, c'est probablement une bonne idée, lorsque vous informez votre appelant d'une condition, cela est totalement inutile.

La culture Python est généralement orientée en faveur des exceptions, mais il est facile d'aller trop loin. Imaginez une list_contains(the_list, item)fonction qui vérifie si la liste contient un élément égal à cet élément. Si le résultat est communiqué via des exceptions, c'est vraiment ennuyeux, car il faut l'appeler comme ceci:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Renvoyer un bool serait beaucoup plus clair:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Si la fonction est déjà censée retourner une valeur, le retour d'une valeur spéciale pour des conditions spéciales est plutôt sujet aux erreurs, car les gens oublieront de vérifier cette valeur (c'est probablement la cause de 1/3 des problèmes en C). Une exception est généralement plus correcte.

Un bon exemple est une pos = find_string(haystack, needle)fonction qui recherche la première occurrence de la needlechaîne dans la chaîne `haystack et retourne la position de départ. Mais que faire si la chaîne de foin ne contient pas la chaîne d'aiguille?

La solution de C et imitée par Python est de renvoyer une valeur spéciale. En C c'est un pointeur nul, en Python c'est -1. Cela conduira à des résultats surprenants lorsque la position est utilisée comme index de chaîne sans vérification, en particulier comme-1 c'est un index valide en Python. En C, votre pointeur NULL vous donnera au moins un défaut de segmentation.

En PHP, une valeur spéciale d'un type différent est retournée: le booléen FALSEau lieu d'un entier. Il s'avère que ce n'est pas mieux en raison des règles de conversion implicites du langage (mais notez qu'en Python, les booléens peuvent également être utilisés comme des entiers!). Les fonctions qui ne renvoient pas un type cohérent sont généralement considérées comme très déroutantes.

Une variante plus robuste aurait été de lever une exception lorsque la chaîne ne peut pas être trouvée, ce qui garantit qu'au cours d'un flux de contrôle normal, il est impossible d'utiliser accidentellement la valeur spéciale à la place d'une valeur ordinaire:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Alternativement, toujours renvoyer un type qui ne peut pas être utilisé directement mais qui doit d'abord être déballé peut être utilisé, par exemple un tuple result-bool où le booléen indique si une exception s'est produite ou si le résultat est utilisable. Alors:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Cela vous oblige à gérer les problèmes immédiatement, mais cela devient très ennuyeux très rapidement. Il vous empêche également de chaîner facilement la fonction. Chaque appel de fonction a désormais besoin de trois lignes de code. Golang est une langue qui pense que cette nuisance vaut la peine d'être sécurisée.

Donc, pour résumer, les exceptions ne sont pas entièrement sans problème et peuvent être définitivement surutilisées, surtout lorsqu'elles remplacent une valeur de retour «normale». Mais lorsqu'elles sont utilisées pour signaler des conditions spéciales (pas nécessairement uniquement des erreurs), les exceptions peuvent vous aider à développer des API propres, intuitives, faciles à utiliser et difficiles à utiliser à mauvais escient.

amon
la source
1
Merci pour la réponse détaillée. Je viens d'un arrière-plan d'autres langages comme Java où "les exceptions ne sont que pour des conditions exceptionnelles", donc c'est nouveau pour moi. Y a-t-il des développeurs principaux de Python qui ont déjà déclaré cela comme ils l'ont fait avec d'autres directives Python comme EAFP, EIBTI, etc. (sur une liste de diffusion, un article de blog, etc.) J'aimerais pouvoir citer quelque chose d'officiel si demande un autre dev / boss. Merci!
JB
En surchargeant la méthode fillInStackTrace de votre classe d'exception personnalisée pour revenir immédiatement, vous pouvez la rendre très bon marché à lever, car c'est la pile qui marche cher et c'est là que cela se fait.
John Cowan
Votre première balle dans votre introduction était déroutante au début. Peut-être pourriez-vous le changer en quelque chose comme "Le code de gestion des exceptions dans le C ++ moderne est sans coût; mais lever une exception coûte cher"? (pour les cacheurs: stackoverflow.com/questions/13835817/… ) [modifier: modifier proposé]
phresnel
Notez que pour les opérations de recherche de dictionnaire en utilisant un collections.defaultdictou my_dict.get(key, default)rend le code beaucoup plus clair quetry: my_dict[key] except: return default
Steve Barnes
11

NON! - pas en général - les exceptions ne sont pas considérées comme de bonnes pratiques de contrôle de flux à l'exception d'une seule classe de code. Le seul endroit où les exceptions sont considérées comme un moyen raisonnable, voire meilleur, de signaler une condition est le fonctionnement du générateur ou de l'itérateur. Ces opérations peuvent renvoyer toute valeur possible en tant que résultat valide, un mécanisme est donc nécessaire pour signaler une fin.

Pensez à lire un fichier binaire de flux un octet à la fois - absolument n'importe quelle valeur est un résultat potentiellement valide mais nous devons toujours signaler une fin de fichier. Nous avons donc le choix, retourner deux valeurs, (la valeur d'octet et un drapeau valide), à ​​chaque fois ou lever une exception quand il n'y a plus rien à faire. Dans les deux cas, le code consommateur peut ressembler à:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

alternativement:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Mais cela, depuis que PEP 343 a été implémenté et porté en arrière, tout a été soigneusement résumé dans la withdéclaration. Ce qui précède devient, le très pythonique:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

En python3, cela est devenu:

for val in open(source, 'rb').read():
     processbyte(val)

Je vous invite fortement à lire le PEP 343 qui donne le contexte, la justification, des exemples, etc.

Il est également habituel d'utiliser une exception pour signaler la fin du traitement lors de l'utilisation des fonctions du générateur pour signaler la fin.

Je voudrais ajouter que votre exemple de chercheur est presque certainement à l'envers, de telles fonctions devraient être des générateurs, renvoyant la première correspondance lors du premier appel, puis des appels de substituant renvoyant la correspondance suivante et levant une NotFoundexception lorsqu'il n'y a plus de correspondances.

Steve Barnes
la source
Vous dites: "NON! - pas en général - les exceptions ne sont pas considérées comme une bonne pratique de contrôle de flux à l'exception d'une seule classe de code". Où est la justification de cette déclaration emphatique? Cette réponse semble se concentrer étroitement sur le cas d'itération. Il existe de nombreux autres cas où les gens pourraient penser à utiliser des exceptions. Quel est le problème avec cela dans ces autres cas?
Daniel Waltrip du
@DanielWaltrip Je vois beaucoup trop de code, dans divers langages de programmation, où les exceptions sont utilisées, souvent beaucoup trop largement, quand un simple ifserait mieux. Quand il s'agit de déboguer et de tester ou même de raisonner sur le comportement de vos codes, il est préférable de ne pas compter sur des exceptions - elles devraient être l'exception. J'ai vu, dans le code de production, un calcul qui retournait via un lancer / rattraper plutôt qu'un simple retour, cela signifiait que lorsque le calcul frappait une division par une erreur de zéro, il renvoyait une valeur aléatoire plutôt que de planter là où l'erreur s'était produite, cela avait causé plusieurs heures de débogage.
Steve Barnes
Hé @SteveBarnes, l'exemple de division par zéro d'erreur ressemble à une mauvaise programmation (avec une capture trop générale qui ne relance pas l'exception).
wTyeRogers
Votre réponse semble se résumer à: A) "NON!" B) il existe des exemples valables où le flux de contrôle via les exceptions fonctionne mieux que les alternatives C) une correction du code de la question pour utiliser des générateurs (qui utilisent les exceptions comme flux de contrôle et le font si bien). Bien que votre réponse soit généralement informative, aucun argument ne semble y figurer pour soutenir le point A, qui, étant en gras et apparemment la déclaration principale, je m'attendrais à un certain soutien. Si elle était prise en charge, je ne sous-évaluerais pas votre réponse. Hélas ...
wTyeRogers