Comment re-lever une exception dans des blocs try / except imbriqués?

107

Je sais que si je veux re-lever une exception, je l'utilise simplement raisesans arguments dans le exceptbloc respectif . Mais étant donné une expression imbriquée comme

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

comment puis-je re-lever le SomeErrorsans casser la trace de la pile? raiseseul, dans ce cas, relèverait le plus récent AlsoFailsError. Ou comment pourrais-je refactoriser mon code pour éviter ce problème?

Tobias Kienzler
la source
2
Avez-vous essayé de mettre plan_Bdans une autre fonction qui retourne Truesur le succès et Falsesur l'exception? Ensuite, le exceptbloc extérieur pourrait simplement êtreif not try_plan_B(): raise
Drew McGowen
@DrewMcGowen Malheureusement, le cas le plus réaliste est que cela se trouve à l'intérieur d'une fonction acceptant des objets arbitraires arget j'essaierais d'appeler, arg.plan_B()ce qui pourrait entraîner un AttributeErrorproblème en raison de argne pas fournir de plan B
Tobias Kienzler
Jetez un œil au module de traceback
Paco
@Paco Merci, je le ferai (bien qu'une réponse montre déjà un moyen plus simple)
Tobias Kienzler
@DrewMcGowen J'ai écrit une réponse basée sur votre commentaire , qui semble moins pythonique que la réponse de user4815162342 . Mais cela est dû au fait que je souhaite également avoir une valeur de retour et permettre plan_Bde lever des exceptions
Tobias Kienzler

Réponses:

128

À partir de Python 3, le traçage est stocké dans l'exception, donc un simple raise efera la bonne chose (principalement):

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

Le traçage produit inclura un avis supplémentaire qui SomeErrors'est produit lors de la manipulation AlsoFailsError(en raison d' raise eêtre à l'intérieur except AlsoFailsError). Ceci est trompeur car ce qui s'est réellement passé est l'inverse: nous l'avons rencontré AlsoFailsErroret géré en essayant de nous en remettre SomeError. Pour obtenir une trace qui n'inclut pas AlsoFailsError, remplacez raise epar raise e from None.

Dans Python 2, vous stockez le type d'exception, la valeur et le suivi dans des variables locales et utilisez la forme à trois arguments deraise :

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
user4815162342
la source
Parfait, c'est ce que je viens de trouver aussi ici , merci! Bien que la suggestion soit raise self.exc_info[1], None, self.exc_info[2]après self.exc_info = sys.exc_info()- mise [1]en première position pour une raison quelconque
Tobias Kienzler
3
@TobiasKienzler raise t, None, tbperdra la valeur de l'exception et forcera raiseà la ré-instancier à partir du type, vous donnant une valeur d'exception moins spécifique (ou simplement incorrecte). Par exemple, si l'exception levée est KeyError("some-key"), elle se relèvera simplement KeyError()et omettra la clé manquante exacte du suivi.
user4815162342
3
@TobiasKienzler Il devrait toujours être possible d'exprimer cela dans Python 3 comme raise v.with_traceback(tb). (Votre commentaire en dit même autant, sauf qu'il propose de ré-instancier la valeur.)
user4815162342
2
De plus, l'avertissement rouge de ne pas stocker sys.exc_info()dans une variable locale avait du sens avant Python 2.0 (publié il y a 13 ans), mais est aujourd'hui ridicule. Le Python moderne serait presque inutile sans le collecteur de cycles, car chaque bibliothèque Python non triviale crée des cycles sans pause et dépend de leur nettoyage correct.
user4815162342
1
@ user4815162342 Vous pouvez supprimer l'erreur imbriquée "une autre erreur s'est produite" en écrivant "lever e de None".
Matthias Urlichs
19

Même si la solution acceptée est correcte, il est bon de pointer vers la bibliothèque Six qui a une solution Python 2 + 3, en utilisant six.reraise.

six. reraise ( exc_type , exc_value , exc_traceback = None)

Relancez une exception, éventuellement avec un retraçage différent. [...]

Ainsi, vous pouvez écrire:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Laurent LAPORTE
la source
1
Bon point - en parlant de Six, vous pouvez également utiliser six.raise_fromsi vous souhaitez inclure des informations qui ont plan_B()également échoué.
Tobias Kienzler
1
@TobiasKienzler: Je pense que c'est une utilisation différente: avec six.raise_fromvous créez une nouvelle exception qui est liée à une précédente, vous ne relancez pas , donc la trace est différente.
Laurent LAPORTE
1
Mon point exactement - si vous reraiseavez l'impression que something()jeté SomeError, si vous raise_fromsavez aussi que cela a causé plan_B()d'être exécuté, mais en jetant le AlsoFailsError. Cela dépend donc du cas d'utilisation. Je pense que raise_fromcela facilitera le débogage
Tobias Kienzler
9

Selon la suggestion de Drew McGowen , mais en prenant soin d'un cas général (où une valeur de retour sest présente), voici une alternative à la réponse de user4815162342 :

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Tobias Kienzler
la source
1
La bonne chose à propos de cette approche est qu'elle fonctionne sans changement dans Python 2 et 3.
user4815162342
2
@ user4815162342 Bon point :) Bien qu'en attendant en Python3 je pense raise from, donc la trace de pile me laisserait également voir le plan B échouer. Qui peut être émulé dans Python 2 d'ailleurs.
Tobias Kienzler le
5

Python 3.5+ attache de toute façon les informations de traçage à l'erreur, il n'est donc plus nécessaire de l'enregistrer séparément.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Matthias Urlichs
la source
2
La question concerne une autre exception se produisant pendant le except. Mais vous avez raison, quand je remplace err = epar, disons, raise AttributeErrorvous obtenez d'abord la SyntaxErrortrace de pile, suivie de a During handling of the above exception, another exception occurred:et de la AttributeErrortrace de pile. Bon à savoir, mais malheureusement, on ne peut pas compter sur l'installation de 3.5+. PS: ff verstehen nicht-Deutsche vermutlich nicht;)
Tobias Kienzler
OK, j'ai donc changé l'exemple pour soulever une autre exception, qui (comme la question initiale demandée) est ignorée lorsque je relance la première.
Matthias Urlichs
3
@TobiasKienzler 3.5+ (auquel je l'ai changé) semble être un format mondialement reconnu. Was denkst du? ;)
linusg
@linusg d'accord :)
Tobias Kienzler