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 with
instruction 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 needle
chaî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 FALSE
au 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.
collections.defaultdict
oumy_dict.get(key, default)
rend le code beaucoup plus clair quetry: my_dict[key] except: return default
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 à:
alternativement:
Mais cela, depuis que PEP 343 a été implémenté et porté en arrière, tout a été soigneusement résumé dans la
with
déclaration. Ce qui précède devient, le très pythonique:En python3, cela est devenu:
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
NotFound
exception lorsqu'il n'y a plus de correspondances.la source
if
serait 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.