Python: Comment puis-je savoir quelles exceptions peuvent être levées à partir d'un appel de méthode

87

Existe-t-il un moyen de savoir (au moment du codage) à quelles exceptions s'attendre lors de l'exécution de code python? Je finis par attraper la classe d'exception de base 90% du temps car je ne sais pas quel type d'exception pourrait être levé (et ne me dites pas de lire la documentation. Plusieurs fois, une exception peut être propagée depuis le plus profond. fois la documentation n'est pas mise à jour ou correcte). Existe-t-il une sorte d'outil pour vérifier cela? (comme en lisant le code python et les bibliothèques)?

GabiMe
la source
2
Gardez à l'esprit qu'en Python <2.6, vous pouvez également raiseutiliser des chaînes, pas seulement des BaseExceptionsous - classes. Donc, si vous appelez du code de bibliothèque hors de votre contrôle, ce except Exceptionn'est même pas suffisant, car il n'attrapera pas les exceptions de chaîne. Comme d'autres l'ont souligné, vous aboyez ici le mauvais arbre.
Daniel Pryden
Je ne savais pas ça. Je pensais sauf Exception: .. attrape presque tout.
GabiMe
2
except Exceptionfonctionne bien pour intercepter les exceptions de chaîne dans Python 2.6 et versions ultérieures.
Jeffrey Harris

Réponses:

22

Je suppose qu'une solution pourrait être imprécise en raison du manque de règles de typage statiques.

Je ne connais pas d'outil qui vérifie les exceptions, mais vous pourriez proposer votre propre outil correspondant à vos besoins (une bonne chance de jouer un peu avec l'analyse statique).

Dans un premier temps, vous pouvez écrire une fonction qui construit un AST, trouve tous les Raisenœuds, puis essaie de trouver des modèles courants de levée d'exceptions (par exemple, appeler un constructeur directement)

Soit xle programme suivant:

x = '''\
if f(x):
    raise IOError(errno.ENOENT, 'not found')
else:
    e = g(x)
    raise e
'''

Construisez l'AST en utilisant le compilerpackage:

tree = compiler.parse(x)

Définissez ensuite une Raiseclasse de visiteurs:

class RaiseVisitor(object):
    def __init__(self):
        self.nodes = []
    def visitRaise(self, n):
        self.nodes.append(n)

Et parcourez les Raisenœuds de collecte AST :

v = RaiseVisitor()
compiler.walk(tree, v)

>>> print v.nodes
[
    Raise(
        CallFunc(
            Name('IOError'),
            [Getattr(Name('errno'), 'ENOENT'), Const('not found')],
            None, None),
        None, None),
    Raise(Name('e'), None, None),
]

Vous pouvez continuer en résolvant les symboles en utilisant les tables de symboles du compilateur, en analysant les dépendances de données, etc. Ou vous pouvez simplement en déduire que CallFunc(Name('IOError'), ...)"devrait certainement signifier augmenter IOError", ce qui est tout à fait acceptable pour des résultats pratiques rapides :)

Andrey Vlasovskikh
la source
Merci pour cette réponse intéressante. Je n'ai pas compris pourquoi devrais-je rechercher autre chose que tous les nœuds de montée. Pourquoi devrais-je "résoudre des symboles à l'aide de tables de symboles du compilateur, analyser les dépendances de données"? Le seul moyen de déclencher une exception n'est-il pas de lever ()?
GabiMe
1
Compte tenu de la v.nodesvaleur ci-dessus, vous ne pouvez pas vraiment dire ce qu'est la chose Name('IOError')ou Name('e'). Vous ne savez pas sur quelle (s) valeur (s) ceux IOError-ci epeuvent pointer, car ce sont des variables dites libres. Même si leur contexte de liaison était connu (ici les tables de symboles entrent en jeu), vous devez effectuer une sorte d'analyse de dépendance des données pour déduire leurs valeurs exactes (cela devrait être difficile en Python).
Andrey Vlasovskikh
Comme vous recherchez une solution semi-automatisée pratique, une liste d' ['IOError(errno.ENOENT, "not found")', 'e']affichage à l'utilisateur est très bien. Mais vous ne pouvez pas déduire de classes réelles de valeurs de variables représentées par des chaînes :) (désolé pour le republication)
Andrey Vlasovskikh
1
Oui. Cette méthode, bien qu'intelligente, ne vous donne pas une couverture complète. En raison de la nature dynamique de Python, il est parfaitement possible (bien que de toute évidence une mauvaise idée) que le code que vous appelez fasse quelque chose comme exc_class = raw_input(); exec "raise " + exc_class. Le fait est que ce type d'analyse statique n'est pas vraiment possible dans un langage dynamique comme Python.
Daniel Pryden
7
À propos, vous pouvez simplement find /path/to/library -name '*.py' | grep 'raise 'obtenir des résultats similaires :)
Andrey Vlasovskikh
24

Vous ne devez attraper que les exceptions que vous allez gérer.

Attraper toutes les exceptions par leurs types concrets est un non-sens. Vous devez détecter les exceptions spécifiques que vous pouvez et que vous allez gérer. Pour les autres exceptions, vous pouvez écrire une capture générique qui attrape "l'exception de base", la consigne (utilisez la str()fonction) et termine votre programme (ou fait quelque chose d'autre qui est approprié dans une situation de crash).

Si vous voulez vraiment gérer toutes les exceptions et êtes sûr qu'aucune d'entre elles n'est fatale (par exemple, si vous exécutez le code dans une sorte d'environnement en bac à sable), alors votre approche de capture de BaseException générique correspond à vos objectifs.

Vous pourriez également être intéressé par une référence d'exception de langue , et non par une référence pour la bibliothèque que vous utilisez.

Si la référence de la bibliothèque est vraiment médiocre et qu'elle ne renvoie pas ses propres exceptions lors de la capture de celles du système, la seule approche utile consiste à exécuter des tests (peut-être l'ajouter à la suite de tests, car si quelque chose n'est pas documenté, cela peut changer!) . Supprimez un fichier crucial pour votre code et vérifiez quelle exception est levée. Fournissez trop de données et vérifiez quelle erreur cela génère.

Vous devrez quand même exécuter des tests, car, même si la méthode d'obtention des exceptions par code source existait, cela ne vous donnerait aucune idée de la façon dont vous devriez gérer l'une d'entre elles . Peut-être que vous devriez afficher le message d'erreur "Le fichier nécessaire.txt est introuvable!" quand tu attrapes IndexError? Seul le test peut le dire.

P Shved
la source
26
Bien sûr, mais comment peut-on décider quelles exceptions il doit gérer s'il ne sait pas ce qui pourrait être lancé?
GabiMe
@ bugspy.net, j'ai corrigé ma réponse pour refléter cette question
P Shved
Il est peut-être temps pour un analyseur de code source de le découvrir? Ne devrait pas être trop difficile à développer, je pense
GabiMe
@ bugspy.net, j'ai enhardi la clause expliquant pourquoi ce n'est peut-être pas le moment.
P Shved
Bien sûr que vous avez raison. Cependant, il peut être intéressant - pendant le développement - de savoir quels types d'exceptions peuvent se produire.
hek2mgl
11

Le bon outil pour résoudre ce problème est unittests. Si vous rencontrez des exceptions déclenchées par du code réel que les unittests ne déclenchent pas, vous avez besoin de plus d'unittests.

Considère ceci

def f(duck):
    try:
        duck.quack()
    except ??? could be anything

le canard peut être n'importe quel objet

Évidemment, vous pouvez avoir un AttributeErrorsi canard n'a pas de charlatan, un TypeErrorsi canard a un charlatan mais il n'est pas appelable. Vous n'avez aucune idée de ce qui duck.quack()pourrait augmenter, peut-être même un DuckErrorou quelque chose

Supposons maintenant que vous ayez un code comme celui-ci

arr[i] = get_something_from_database()

S'il lève un, IndexErrorvous ne savez pas s'il vient de arr [i] ou du plus profond de la fonction de base de données. généralement, peu importe où l'exception s'est produite, plutôt que quelque chose s'est mal passé et que ce que vous vouliez se produire ne s'est pas produit.

Une technique pratique consiste à attraper et peut-être à relancer l'exception comme ceci

except Exception as e
    #inspect e, decide what to do
    raise
John La Rooy
la source
Pourquoi l'attraper si vous allez le «relancer»?
Tarnay Kálmán
Il n'est pas nécessaire de le relancer, c'est ce que le commentaire était censé indiquer.
John La Rooy
2
Vous pouvez également choisir de consigner l'exception quelque part, puis de relancer
John La Rooy
2
Je ne pense pas que l'écriture de tests unitaires soit la réponse. La question est "comment puis-je savoir à quelles exceptions m'attendre" et l'écriture de tests unitaires ne vous aidera pas à le découvrir. En fait, pour écrire le test unitaire, vous devez déjà savoir à quelles exceptions vous attendre afin d'écrire un test unitaire correct, vous devez également répondre à la question d'origine.
Bruno Ranschaert
6

Personne n'a expliqué jusqu'à présent, pourquoi vous ne pouvez pas avoir une liste d'exceptions complète et correcte à 100%, alors j'ai pensé que cela méritait d'être commenté. L'une des raisons est une fonction de première classe. Disons que vous avez une fonction comme celle-ci:

def apl(f,arg):
   return f(arg)

Maintenant aplpeut soulever toute exception qui se flève. Bien qu'il n'y ait pas beaucoup de fonctions comme celle-là dans la bibliothèque principale, tout ce qui utilise la compréhension de liste avec des filtres personnalisés, mapper, réduire, etc. est affecté.

La documentation et les analyseurs de sources sont ici les seules sources d'informations «sérieuses». Gardez simplement à l'esprit ce qu'ils ne peuvent pas faire.

viraptor
la source
4

Je suis tombé sur cela lors de l'utilisation de socket, je voulais découvrir toutes les conditions d'erreur dans lesquelles je me heurterais (donc plutôt que d'essayer de créer des erreurs et de déterminer quelle socket je voulais juste une liste concise). Finalement, j'ai fini par grep'ing "/usr/lib64/python2.4/test/test_socket.py" pour "augmenter":

$ grep raise test_socket.py
Any exceptions raised by the clients during their tests
        raise TypeError, "test_func must be a callable function"
    raise NotImplementedError, "clientSetUp must be implemented."
    def raise_error(*args, **kwargs):
        raise socket.error
    def raise_herror(*args, **kwargs):
        raise socket.herror
    def raise_gaierror(*args, **kwargs):
        raise socket.gaierror
    self.failUnlessRaises(socket.error, raise_error,
    self.failUnlessRaises(socket.error, raise_herror,
    self.failUnlessRaises(socket.error, raise_gaierror,
        raise socket.error
    # Check that setting it to an invalid value raises ValueError
    # Check that setting it to an invalid type raises TypeError
    def raise_timeout(*args, **kwargs):
    self.failUnlessRaises(socket.timeout, raise_timeout,
    def raise_timeout(*args, **kwargs):
    self.failUnlessRaises(socket.timeout, raise_timeout,

Ce qui est une liste d'erreurs assez concise. Maintenant, bien sûr, cela ne fonctionne qu'au cas par cas et dépend de l'exactitude des tests (ce qu'ils sont généralement). Sinon, vous devez à peu près attraper toutes les exceptions, les consigner, les disséquer et comprendre comment les gérer (ce qui avec les tests unitaires ne serait pas trop difficile).

Kurt
la source
4
Cela renforce mon argument, que la gestion des exceptions en Python est très problématique, si nous devons utiliser grep ou des analyseurs de source pour traiter quelque chose d'aussi basique (qui par exemple en java existait depuis le premier jour. Parfois, la verbosité est une bonne chose. Java est verbeux mais au moins il n'y a pas de mauvaises surprises)
GabiMe
@GabiMe, ce n'est pas comme si cette capacité (ou la saisie statique en général) est une solution miracle pour éviter tous les bugs. Java est plein de mauvaises surprises. C'est pourquoi eclipse plante régulièrement.
John La Rooy
2

Il y a deux façons que j'ai trouvées informatives. Le premier, exécutez le code dans iPython, qui affichera le type d'exception.

n = 2
str = 'me '
str + 2
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Dans un deuxième temps, nous nous contentons de trop attraper et nous l'améliorons avec le temps. Incluez une tryexpression dans votre code et attrapez except Exception as err. Imprimez suffisamment de données pour savoir quelle exception a été levée. Au fur et à mesure que des exceptions sont levées, améliorez votre code en ajoutant une exceptclause plus précise . Lorsque vous sentez que vous avez détecté toutes les exceptions pertinentes, supprimez celle tout compris. Une bonne chose à faire quand même car ça avale les erreurs de programmation.

try:
   so something
except Exception as err:
   print "Some message"
   print err.__class__
   print err
   exit(1)
Rahav
la source
1

normalement, vous n'avez besoin d'attraper une exception que sur quelques lignes de code. Vous ne voudriez pas mettre toute votre mainfonction dans l' try exceptarticle. pour toutes les quelques lignes, vous devriez toujours maintenant (ou être en mesure de vérifier facilement) quel type d'exception pourrait être déclenché.

docs ont une liste exhaustive d'exceptions intégrées . n'essayez pas d'exclure les exceptions auxquelles vous ne vous attendez pas, elles pourraient être gérées / attendues dans le code d'appel.

edit : ce qui pourrait être jeté dépend évidemment de ce que vous faites! accès à un élément aléatoire d'une séquence IndexError:, élément aléatoire d'un dict:, KeyErroretc.

Essayez simplement d'exécuter ces quelques lignes dans IDLE et de provoquer une exception. Mais unittest serait une meilleure solution, naturellement.

SilentGhost
la source
1
Cela ne répond pas à ma simple question. Je ne demande pas comment concevoir ma gestion des exceptions, ni quand ni comment intercepter. Je demande comment savoir ce qui pourrait être jeté
GabiMe
1
@ bugspy.net: Il est impossible de faire ce que vous demandez, et c'est une solution de contournement parfaitement valable.
Daniel Pryden