Extraer información de rastreo de un objeto de excepción

111

Dado un objeto de excepción (de origen desconocido), ¿hay forma de obtener su rastreo? Tengo un código como este:

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

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

¿Cómo puedo extraer el rastreo del objeto Exception una vez que lo tengo?

georg
fuente

Respuestas:

92

La respuesta a esta pregunta depende de la versión de Python que esté usando.

En Python 3

Es simple: las excepciones vienen equipadas con un __traceback__atributo que contiene el rastreo. Este atributo también se puede escribir y se puede configurar convenientemente utilizando el with_tracebackmétodo de excepciones:

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

Estas características se describen mínimamente como parte de la raisedocumentación.

Todo el crédito por esta parte de la respuesta debe ir a Vyctor, quien primero publicó esta información . Lo incluyo aquí solo porque esta respuesta está atascada en la parte superior y Python 3 se está volviendo más común.

En Python 2

Es fastidiosamente complejo. El problema con los tracebacks es que tienen referencias a marcos de pila, y los marcos de pila tienen referencias a los tracebacks que tienen referencias a marcos de pila que tienen referencias a ... ya se hace una idea. Esto causa problemas al recolector de basura. (Gracias a ecatmur por señalar esto primero).

La buena forma de resolver esto sería romper quirúrgicamente el ciclo después de salir de la exceptcláusula, que es lo que hace Python 3. La solución Python 2 es mucho más fea: se le proporciona una función ad-hoc sys.exc_info(), que solo funciona dentro de la except cláusula . Devuelve una tupla que contiene la excepción, el tipo de excepción y el rastreo de cualquier excepción que se esté manejando actualmente.

Entonces, si está dentro de la exceptcláusula, puede usar la salida de sys.exc_info()junto con el tracebackmódulo para hacer varias cosas útiles:

>>> 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

Pero como indica su edición, está intentando obtener el rastreo que se habría impreso si su excepción no se hubiera manejado, después de que ya se haya manejado. Esa es una pregunta mucho más difícil. Desafortunadamente, sys.exc_inforegresa (None, None, None)cuando no se maneja ninguna excepción. Otros sysatributos relacionados tampoco ayudan. sys.exc_tracebackestá en desuso y no está definido cuando no se maneja ninguna excepción; sys.last_tracebackparece perfecto, pero parece que solo se define durante las sesiones interactivas.

Si puede controlar cómo se genera la excepción, es posible que pueda usar inspectuna excepción personalizada para almacenar parte de la información. Pero no estoy del todo seguro de cómo funcionaría.

A decir verdad, detectar y devolver una excepción es algo inusual. Esto podría ser una señal de que necesita refactorizar de todos modos.

remitente
fuente
Estoy de acuerdo en que devolver excepciones es de alguna manera poco convencional, pero vea mi otra pregunta para conocer la justificación de esto.
georg
@ thg435, ok, esto tiene más sentido entonces. Considere mi solución anterior sys.exc_infojunto con el enfoque de devolución de llamada que sugiero en su otra pregunta.
remitente
69

Desde Python 3.0 [PEP 3109], la clase incorporada Exceptiontiene un __traceback__atributo que contiene un traceback object(con Python 3.2.3):

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

El problema es que después de buscar en Google__traceback__ por un tiempo, encontré solo algunos artículos, pero ninguno de ellos describe si debería (no) usarlos o por qué __traceback__.

Sin embargo, la documentación de Python 3raise dice que:

Normalmente, un objeto de rastreo se crea automáticamente cuando se genera una excepción y se le adjunta como __traceback__atributo, que se puede escribir.

Así que supongo que está destinado a ser utilizado.

Vyktor
fuente
4
Sí, está destinado a ser utilizado. De Novedades de Python 3.0 "PEP 3134: los objetos de excepción ahora almacenan su rastreo como el atributo de rastreo . Esto significa que un objeto de excepción ahora contiene toda la información relacionada con una excepción, y hay menos razones para usar sys.exc_info () ( aunque este último no se elimina) ".
Maciej Szpakowski
Realmente no entiendo por qué esta respuesta es tan vacilante y equívoca. Es una propiedad documentada; ¿Por qué no estaría "destinado a ser utilizado"?
Mark Amery
2
@MarkAmery ¿Posiblemente el __nombre indica que es un detalle de implementación, no una propiedad pública?
Básico
4
@Basic, eso no es lo que indica aquí. Convencionalmente en Python __fooes un método privado pero __foo__(con guiones bajos al final también) es un método "mágico" (y no privado).
Mark Amery
1
Para su información, el __traceback__atributo es 100% seguro de usar como desee, sin implicaciones de GC. Es difícil saberlo por la documentación, pero ecatmur encontró pruebas contundentes .
remitente
38

Una forma de obtener el rastreo como una cadena de un objeto de excepción en 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(...)devuelve una lista de cadenas. ''.join(...)los une. Para obtener más referencias, visite: https://docs.python.org/3/library/traceback.html#traceback.format_tb

Hieu
fuente
21

Por otro lado, si realmente desea obtener el rastreo completo como lo vería impreso en su terminal, desea esto:

>>> 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 usa las format_tbrespuestas anteriores sugieren que obtendrá menos información:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>
Daniel Porteous
fuente
4
¡Finalmente! Esta debería ser la mejor respuesta. ¡Gracias, Daniel!
Dany
3
Argh, había pasado los últimos 20 minutos tratando de resolver esto antes de encontrar esto :-) etype=type(exc)se puede omitir ahora por cierto: "Cambiado en la versión 3.5: El argumento etype se ignora y se infiere del tipo de valor". docs.python.org/3.7/library/… Probado en Python 3.7.3.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

Hay una muy buena razón por la que el rastreo no se almacena en la excepción; Debido a que el rastreo contiene referencias a los locales de su pila, esto daría como resultado una referencia circular y una fuga de memoria (temporal) hasta que el GC circular entre en funcionamiento (es por eso que nunca debe almacenar el rastreo en una variable local ).

Lo único en lo que puedo pensar es que montes stufflos globales de parche para que cuando crea que está detectando, en realidad esté detectando Exceptionun tipo especializado y la excepción se propague a ti como llamador:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()
ecatmur
fuente
7
Esto está mal. Python 3 coloca el objeto de rastreo en la excepción, como e.__traceback__.
Glenn Maynard
6
@GlennMaynard Python 3 resuelve el problema al eliminar el objetivo de excepción al salir del exceptbloque, según PEP 3110 .
ecatmur