Extraire les informations de trace d'un objet d'exception

111

Étant donné un objet Exception (d'origine inconnue), y a-t-il moyen d'obtenir son traceback? J'ai un code comme celui-ci:

def stuff():
   try:
       .....
       return useful
   except Exception as e:
       return e

result = stuff()
if isinstance(result, Exception):
    result.traceback <-- How?

Comment puis-je extraire le traçage de l'objet Exception une fois que je l'ai?

Georg
la source

Réponses:

92

La réponse à cette question dépend de la version de Python que vous utilisez.

Dans Python 3

C'est simple: les exceptions sont équipées d'un __traceback__attribut qui contient le traçage. Cet attribut est également accessible en écriture et peut être facilement défini à l'aide de la with_tracebackméthode des exceptions:

raise Exception("foo occurred").with_traceback(tracebackobj)

Ces fonctionnalités sont décrites de manière minimale dans le cadre de la raisedocumentation.

Tout le mérite de cette partie de la réponse doit revenir à Vyctor, qui a publié cette information en premier . Je l'inclus ici uniquement parce que cette réponse est bloquée en haut et que Python 3 est de plus en plus courant.

Dans Python 2

C'est extrêmement complexe. Le problème avec les retraits est qu'ils ont des références aux cadres de pile, et les cadres de pile ont des références aux retraçages qui ont des références aux cadres de pile qui ont des références à ... vous voyez l'idée. Cela provoque des problèmes pour le garbage collector. (Merci à ecatmur d' avoir d'abord signalé cela.)

La bonne façon de résoudre ce problème serait de rompre chirurgicalement le cycle après avoir quitté la exceptclause, ce que fait Python 3. La solution Python 2 est beaucoup plus moche: vous disposez d'une fonction ad-hoc sys.exc_info(), qui ne fonctionne qu'à l'intérieur de la except clause . Il retourne un tuple contenant l'exception, le type d'exception et la trace de toute exception actuellement gérée.

Donc, si vous êtes à l'intérieur de la exceptclause, vous pouvez utiliser la sortie de sys.exc_info()avec le tracebackmodule pour faire diverses choses utiles:

>>> import sys, traceback
>>> def raise_exception():
...     try:
...         raise Exception
...     except Exception:
...         ex_type, ex, tb = sys.exc_info()
...         traceback.print_tb(tb)
...     finally:
...         del tb
... 
>>> raise_exception()
  File "<stdin>", line 3, in raise_exception

Mais comme votre modification l'indique, vous essayez d'obtenir le traçage qui aurait été imprimé si votre exception n'avait pas été gérée, après l'avoir déjà gérée. C'est une question beaucoup plus difficile. Malheureusement, sys.exc_inforetourne (None, None, None)lorsqu'aucune exception n'est gérée. D'autres sysattributs connexes n'aident pas non plus. sys.exc_tracebackest obsolète et indéfinie lorsqu'aucune exception n'est gérée; sys.last_tracebacksemble parfait, mais il ne semble être défini que lors de sessions interactives.

Si vous pouvez contrôler la manière dont l'exception est déclenchée, vous pourrez peut-être utiliser inspectune exception personnalisée pour stocker certaines des informations. Mais je ne suis pas tout à fait sûr de savoir comment cela fonctionnerait.

Pour dire la vérité, attraper et renvoyer une exception est quelque chose d'inhabituel à faire. Cela pourrait être un signe que vous devez de toute façon refactoriser.

expéditeur
la source
Je conviens que le retour d'exceptions est en quelque sorte non conventionnel, mais voyez mon autre question pour une explication derrière cela.
georg
@ thg435, ok, cela a plus de sens alors. Considérez ma solution ci-dessus sys.exc_infoen conjonction avec l' approche de rappel que je suggère sur votre autre question.
senderle
Plus d'informations (il y en a très peu) sur les objets de trace: docs.python.org/3/library/types.html#types.TracebackType docs.python.org/3/reference/datamodel.html#traceback-objects
0xfede7c8
69

Depuis Python 3.0 [PEP 3109], la classe intégrée Exceptiona un __traceback__attribut qui contient un traceback object(avec Python 3.2.3):

>>> try:
...     raise Exception()
... except Exception as e:
...     tb = e.__traceback__
...
>>> tb
<traceback object at 0x00000000022A9208>

Le problème est qu'après avoir cherché__traceback__ sur Google pendant un certain temps, je n'ai trouvé que quelques articles mais aucun d'entre eux ne décrit si ou pourquoi vous devriez (ne pas) utiliser __traceback__.

Cependant, la documentation Python 3 pourraise dit que:

Un objet de trace est normalement créé automatiquement lorsqu'une exception est déclenchée et attachée à lui en tant __traceback__qu'attribut, qui est accessible en écriture.

Je suppose donc qu'il est destiné à être utilisé.

Vyktor
la source
4
Oui, il est destiné à être utilisé. From What's New In Python 3.0 "PEP 3134: Les objets d'exception stockent désormais leur traceback en tant qu'attribut traceback . Cela signifie qu'un objet d'exception contient désormais toutes les informations relatives à une exception, et il y a moins de raisons d'utiliser sys.exc_info () ( bien que ce dernier ne soit pas supprimé). "
Maciej Szpakowski
Je ne comprends pas vraiment pourquoi cette réponse est si hésitante et équivoque. C'est une propriété documentée; pourquoi ne serait-il pas «destiné à être utilisé»?
Mark Amery
2
@MarkAmery Peut-être le __dans le nom indiquant que c'est un détail d'implémentation, pas une propriété publique?
base
4
@Basic ce n'est pas ce qu'il indique ici. Conventionnellement en Python __fooest une méthode privée mais __foo__(avec des traits de soulignement à la fin aussi) est une méthode "magique" (et non privée).
Mark Amery
1
FYI, l' __traceback__attribut est 100% sûr à utiliser comme vous le souhaitez, sans implication GC. Il est difficile de dire cela à partir de la documentation, mais ecatmur a trouvé des preuves tangibles .
senderle
38

Un moyen d'obtenir le traçage sous forme de chaîne à partir d'un objet d'exception dans Python 3:

import traceback

# `e` is an exception object that you get from somewhere
traceback_str = ''.join(traceback.format_tb(e.__traceback__))

traceback.format_tb(...)renvoie une liste de chaînes. ''.join(...)les réunit. Pour plus d'informations, veuillez visiter: https://docs.python.org/3/library/traceback.html#traceback.format_tb

Hieu
la source
21

En passant, si vous voulez réellement obtenir le traçage complet tel que vous le verriez imprimé sur votre terminal, vous voulez ceci:

>>> try:
...     print(1/0)
... except Exception as e:
...     exc = e
...
>>> exc
ZeroDivisionError('division by zero')
>>> tb_str = traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__)
>>> tb_str
['Traceback (most recent call last):\n', '  File "<stdin>", line 2, in <module>\n', 'ZeroDivisionError: division by zero\n']
>>> print("".join(tb_str))
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

Si vous utilisez format_tbles réponses ci-dessus, vous obtiendrez moins d'informations:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>
Daniel Porteous
la source
4
Finalement! Cela devrait être la meilleure réponse. Merci Daniel!
Dany
3
Argh, j'avais passé les 20 dernières minutes à essayer de comprendre cela avant de trouver ceci :-) etype=type(exc)peut être omis maintenant btw: "Modifié dans la version 3.5: l'argument etype est ignoré et déduit du type de valeur." docs.python.org/3.7/library/... Testé en Python 3.7.3.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

Il y a une très bonne raison pour laquelle le retraçage n'est pas stocké dans l'exception; car le retraçage contient des références aux locaux de sa pile, cela entraînerait une référence circulaire et une fuite de mémoire (temporaire) jusqu'à ce que le GC circulaire entre en jeu. (C'est pourquoi vous ne devriez jamais stocker le retraçage dans une variable locale .)

La seule chose à laquelle je puisse penser serait que vous mesuriez stuffles globaux de monkeypatch afin que, quand il pense qu'il attrape, Exceptionil attrape en fait un type spécialisé et l'exception se propage à vous en tant qu'appelant:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()
ecatmur
la source
7
C'est faux. Python 3 place l'objet traceback dans l'exception, comme e.__traceback__.
Glenn Maynard
6
@GlennMaynard Python 3 résout le problème en supprimant la cible d'exception à la sortie du exceptbloc, conformément à PEP 3110 .
ecatmur