web-dev-qa-db-fra.com

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

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 fait 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é de 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 aux déclarations des développeurs.

Cela ( 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 indiquent également que ce style est Pythonic. Sur quoi est-ce basé?

TIA

17
J B

Le consensus général "n'utilisez pas d'exceptions!" vient principalement d'autres langues et même parfois il est dépassé.

  • En C++, lancer une exception est très coûteux en raison du "déroulement de la pile". Chaque déclaration de variable locale est comme une instruction with 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 d'exception: à moins que vous n'utilisiez réellement RAII, il est facile de fuir 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 la 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 ex. car il n'était utilisé que pour le contrôle du flux.

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à de certains frais généraux qui rendent le coût des exceptions imperceptible) 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 aussi rapide que d'accéder simplement à l'entrée the_dict[key]; ... et vérifier si vous obtenez une erreur de clé. les fonctionnalités du langage intégral (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, la question se pose toujours de savoir si vous devez les utiliser au lieu 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 fonction list_contains(the_list, item) 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 renvoyer 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 fonction pos = find_string(haystack, needle) qui recherche la première occurrence de la chaîne needle dans la chaîne `haystack et retourne la position de départ. Mais que se passe-t-il si la chaîne de foin ne contient pas la chaîne d'aiguille?

La solution par C et imitée par Python est de renvoyer une valeur spéciale. En C, il s'agit d'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, d'autant plus que -1 Est un index valide en Python. En C, votre pointeur NULL vous donnera au moins une erreur de segmentation.

En PHP, une valeur spéciale d'un type différent est retournée: le booléen FALSE au lieu d'un entier. Comme il s'avère que ce n'est pas mieux en raison des règles de conversion implicites du langage (mais notez que dans Python ainsi les booléens peuvent être utilisés comme des entiers!). Fonctions qui ne le font pas renvoyer un type cohérent sont généralement considérés comme très déroutants.

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 résultat-bool Tuple 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 agaçant 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 seulement des erreurs), les exceptions peuvent vous aider à développer des API propres, intuitives, faciles à utiliser et difficiles à utiliser à mauvais escient.

29
amon

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 . 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 comme 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 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 a, depuis PEP 34 a été implémenté et porté en arrière, tous ont été soigneusement emballés dans l'instruction with . Ce qui précède devient, 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 PEP 34 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, ces fonctions devraient être des générateurs, renvoyant la première correspondance lors du premier appel, puis des appels de substitution renvoyant la correspondance suivante et levant une exception NotFound lorsqu'il y a non plus correspond.

11
Steve Barnes