Extraia informações de traceback de um objeto de exceção

111

Dado um objeto Exception (de origem desconhecida), existe uma maneira de obter seu rastreio? Eu tenho um código como este:

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

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

Como posso extrair o traceback do objeto Exception depois de tê-lo?

georg
fonte

Respostas:

92

A resposta a esta pergunta depende da versão do Python que você está usando.

Em Python 3

É simples: as exceções vêm equipadas com um __traceback__atributo que contém o traceback. Este atributo também é gravável e pode ser convenientemente configurado usando o with_tracebackmétodo de exceções:

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

Esses recursos são minimamente descritos como parte da raisedocumentação.

Todo o crédito por esta parte da resposta deve ir para Vyctor, que primeiro postou esta informação . Estou incluindo aqui apenas porque essa resposta está presa no topo e o Python 3 está se tornando mais comum.

Em Python 2

É irritantemente complexo. O problema com tracebacks é que eles têm referências a stack frames, e stack frames têm referências a tracebacks que têm referências a stack frames que têm referências a ... você entendeu. Isso causa problemas para o coletor de lixo. (Obrigado a ecatmur por primeiro apontar isso.)

A boa maneira de resolver isso seria quebrar cirurgicamente o ciclo depois de deixar a exceptcláusula, que é o que o Python 3 faz. A solução Python 2 é muito mais feia: você recebe uma função ad-hoc sys.exc_info(), que só funciona dentro da except cláusula . Ele retorna uma tupla contendo a exceção, o tipo de exceção e o rastreamento para qualquer exceção que esteja sendo tratada no momento.

Portanto, se você estiver dentro da exceptcláusula, poderá usar a saída de sys.exc_info()junto com o tracebackmódulo para fazer várias coisas úteis:

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

Mas, como indica sua edição, você está tentando obter o rastreamento que teria sido impresso se sua exceção não tivesse sido tratada, depois de ter sido tratada. Essa é uma pergunta muito mais difícil. Infelizmente, sys.exc_inforetorna (None, None, None)quando nenhuma exceção está sendo tratada. Outros sysatributos relacionados também não ajudam. sys.exc_tracebacké preterido e indefinido quando nenhuma exceção está sendo tratada; sys.last_tracebackparece perfeito, mas parece ser definido apenas durante as sessões interativas.

Se você puder controlar como a exceção é gerada, você poderá usar inspectuma exceção personalizada para armazenar algumas das informações. Mas não tenho certeza de como isso funcionaria.

Para dizer a verdade, capturar e retornar uma exceção é algo incomum. Isso pode ser um sinal de que você precisa refatorar de qualquer maneira.

remetente
fonte
Eu concordo que retornar exceções não é de alguma forma convencional, mas veja minha outra pergunta para alguma justificativa por trás disso.
georg
@ thg435, ok, isso faz mais sentido então. Considere minha solução acima usando sys.exc_infoem conjunto com a abordagem de retorno de chamada que sugiro em sua outra pergunta.
remetente
69

Desde Python 3.0 [PEP 3109], a classe incorporada Exceptiontem um __traceback__atributo que contém um traceback object(com Python 3.2.3):

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

O problema é que depois de pesquisar__traceback__ por um tempo no Google encontrei apenas alguns artigos, mas nenhum deles descreve se ou por que você deve (não) usar __traceback__.

No entanto, a documentaçãoraise do Python 3 para diz que:

Um objeto traceback normalmente é criado automaticamente quando uma exceção é levantada e anexada a ele como o __traceback__atributo, que é gravável.

Portanto, presumo que seja para ser usado.

Vyktor
fonte
4
Sim, deve ser usado. Do que há de novo no Python 3.0 "PEP 3134: Objetos de exceção agora armazenam seu traceback como o atributo traceback . Isso significa que um objeto de exceção agora contém todas as informações pertencentes a uma exceção e há menos razões para usar sys.exc_info () ( embora o último não seja removido). "
Maciej Szpakowski
Eu realmente não entendo por que essa resposta é tão hesitante e ambígua. É uma propriedade documentada; por que não seria "feito para ser usado"?
Mark Amery
2
@MarkAmery Possivelmente __no nome indicando que é um detalhe de implementação, não uma propriedade pública?
Básico de
4
@Basic não é o que indica aqui. Convencionalmente, em Python __fooé um método privado, mas __foo__(com sublinhados também) é um método "mágico" (e não privado).
Mark Amery
1
Para sua informação, o __traceback__atributo é 100% seguro para usar como quiser, sem implicações de GC. É difícil dizer isso pela documentação, mas ecatmur encontrou evidências concretas .
remetente
38

Uma maneira de obter traceback como uma string de um objeto de exceção no 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(...)retorna uma lista de strings. ''.join(...)os junta. Para obter mais referências, visite: https://docs.python.org/3/library/traceback.html#traceback.format_tb

Hieu
fonte
21

Como um aparte, se você deseja realmente obter o rastreamento completo como você o veria impresso em seu terminal, você deseja isto:

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

Se você usar format_tbas respostas acima, sugere que obterá menos informações:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>
Daniel Porteous
fonte
4
Finalmente! Esta deve ser a melhor resposta. Obrigada Daniel!
Dany
3
Argh, eu passei os últimos 20 minutos tentando descobrir isso antes de descobrir que :-) etype=type(exc)pode ser omitido agora btw: "Alterado na versão 3.5: O argumento etype é ignorado e inferido do tipo de valor." docs.python.org/3.7/library/… Testado em Python 3.7.3.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

Há uma boa razão para o traceback não ser armazenado na exceção; porque o traceback contém referências aos locais de sua pilha, isso resultaria em uma referência circular e vazamento de memória (temporário) até que o GC circular seja acionado. (É por isso que você nunca deve armazenar o traceback em uma variável local .)

Praticamente a única coisa em que posso pensar seria que você fizesse stuffum monkeypatch para os globais de modo que, quando ele pensa que está capturando Exception, está na verdade capturando um tipo especializado e a exceção se propaga para você como o chamador:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()
ecatmur
fonte
7
Isto está errado. Python 3 coloca o objeto traceback na exceção, como e.__traceback__.
Glenn Maynard
6
@GlennMaynard Python 3 resolve o problema excluindo o alvo da exceção ao sair do exceptbloco, de acordo com PEP 3110 .
ecatmur