Uzyskaj informacje o śledzeniu z obiektu wyjątku

111

Biorąc pod uwagę obiekt Exception (nieznanego pochodzenia), czy istnieje sposób na uzyskanie jego śledzenia? Mam taki kod:

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

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

Jak mogę wyodrębnić dane śledzenia z obiektu Exception, gdy już je mam?

Georg
źródło

Odpowiedzi:

92

Odpowiedź na to pytanie zależy od używanej wersji Pythona.

W Pythonie 3

To proste: wyjątki są wyposażone w __traceback__atrybut zawierający dane śledzenia. Ten atrybut jest również zapisywalny i można go wygodnie ustawić za pomocą with_tracebackmetody wyjątków:

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

Te funkcje są minimalnie opisane jako część raisedokumentacji.

Cała zasługa tej części odpowiedzi należy się Vyctorowi, który jako pierwszy opublikował te informacje . Uwzględniam to tutaj tylko dlatego, że ta odpowiedź utknęła na górze, a Python 3 staje się coraz bardziej powszechny.

W Pythonie 2

To irytująco złożone. Problem z trackbackami polega na tym, że mają one odniesienia do ramek stosu, a ramki stosu mają odniesienia do elementów traceback, które mają odniesienia do ramek stosu, które mają odniesienia do ... rozumiesz. Powoduje to problemy dla odśmiecacza. (Dzięki ecatmur za pierwsze wskazanie).

Dobrym sposobem rozwiązania tego problemu byłoby chirurgiczne przerwanie cyklu po opuszczeniu exceptklauzuli, co robi Python 3. Rozwiązanie w Pythonie 2 jest dużo brzydsze: masz do dyspozycji funkcję ad-hoc sys.exc_info(), która działa tylko wewnątrz except klauzuli . Zwraca krotkę zawierającą wyjątek, typ wyjątku i ślad zwrotny dla dowolnego wyjątku, który jest obecnie obsługiwany.

Więc jeśli jesteś wewnątrz exceptklauzuli, możesz użyć danych wyjściowych sys.exc_info()wraz z tracebackmodułem do zrobienia różnych przydatnych rzeczy:

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

Ale jak wskazuje twoja edycja, próbujesz uzyskać dane śledzenia, które zostałyby wydrukowane, gdyby twój wyjątek nie został obsłużony, po tym, jak został już obsłużony. To znacznie trudniejsze pytanie. Niestety sys.exc_infowraca, (None, None, None)gdy żaden wyjątek nie jest obsługiwany. Inne powiązane sysatrybuty też nie pomagają. sys.exc_tracebackjest przestarzały i niezdefiniowany, gdy żaden wyjątek nie jest obsługiwany; sys.last_tracebackwydaje się idealny, ale wydaje się, że jest definiowany tylko podczas sesji interaktywnych.

Jeśli możesz kontrolować sposób wywoływania wyjątku, możesz użyć inspecti niestandardowego wyjątku do przechowywania niektórych informacji. Ale nie jestem do końca pewien, jak to zadziała.

Prawdę mówiąc, wyłapywanie i zwracanie wyjątku jest czymś niezwykłym. Może to oznaczać, że i tak musisz dokonać refaktoryzacji.

nadawca
źródło
Zgadzam się, że zwracanie wyjątków jest w jakiś sposób niekonwencjonalne, ale zobacz moje inne pytanie, aby uzasadnić to.
georg
@ thg435, ok, to ma większy sens. Rozważ moje powyższe rozwiązanie sys.exc_infow połączeniu z podejściem oddzwaniania, które proponuję w przypadku innego pytania.
nadawca
69

Od Pythona 3.0 [PEP 3109] wbudowana klasa Exceptionma __traceback__atrybut, który zawiera traceback object(w Pythonie 3.2.3):

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

Problem w tym, że po dłuższym szukaniu__traceback__ w Google znalazłem tylko kilka artykułów, ale żaden z nich nie opisuje, czy i dlaczego powinieneś (nie) używać __traceback__.

Jednak dokumentacja Python 3 dlaraise mówi, że:

Obiekt śledzenia jest zwykle tworzony automatycznie, gdy zostanie zgłoszony wyjątek i dołączony do niego jako __traceback__atrybut, który jest zapisywalny.

Więc zakładam, że ma być używany.

Vyktor
źródło
4
Tak, jest przeznaczony do użytku. Od Co nowego w Pythonie 3.0 "PEP 3134: Obiekty wyjątków przechowują teraz swoje dane śledzenia jako atrybut śledzenia . Oznacza to, że obiekt wyjątku zawiera teraz wszystkie informacje dotyczące wyjątku i jest mniej powodów, aby używać sys.exc_info () ( choć ta ostatnia nie jest usuwana). "
Maciej Szpakowski
Naprawdę nie rozumiem, dlaczego ta odpowiedź jest tak niepewna i dwuznaczna. To udokumentowana nieruchomość; dlaczego to nie być „przeznaczone do pracy”?
Mark Amery,
2
@MarkAmery Być może __w nazwie znajduje się informacja, że ​​jest to szczegół implementacji, a nie właściwość publiczna?
Podstawowy
4
@Basic nie o to tutaj chodzi. Tradycyjnie w Pythonie __foojest to metoda prywatna, ale __foo__(również z końcowymi podkreśleniami) jest metodą „magiczną” (a nie prywatną).
Mark Amery
1
FYI, __traceback__atrybut jest w 100% bezpieczny w użyciu w dowolny sposób, bez wpływu na GC. Trudno to stwierdzić z dokumentacji, ale ecatmur znalazł mocne dowody .
nadawca
38

Sposób na uzyskanie danych śledzenia w postaci ciągu znaków z obiektu wyjątku w Pythonie 3:

import traceback

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

traceback.format_tb(...)zwraca listę ciągów. ''.join(...)łączy je razem. Więcej informacji można znaleźć pod adresem : https://docs.python.org/3/library/traceback.html#traceback.format_tb

Hieu
źródło
21

Na marginesie, jeśli chcesz faktycznie uzyskać pełne śledzenie w postaci wydrukowanej na terminalu, potrzebujesz tego:

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

Jeśli zastosujesz format_tbpowyższe odpowiedzi, otrzymasz mniej informacji:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>
Daniel Porteous
źródło
4
Wreszcie! To powinna być najlepsza odpowiedź. Dziękuję Daniel!
Dany
3
Argh, spędziłem ostatnie 20 minut próbując to rozgryźć, zanim znalazłem to :-) etype=type(exc)można teraz pominąć btw: "Zmieniono w wersji 3.5: argument etype jest ignorowany i wywnioskowany z typu wartości." docs.python.org/3.7/library/ ... Przetestowano w Pythonie 3.7.3.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

Jest bardzo dobry powód, dla którego śledzenie nie jest przechowywane w wyjątku; ponieważ traceback przechowuje odniesienia do ustawień lokalnych swojego stosu, spowodowałoby to cykliczne odwołanie i (tymczasowy) wyciek pamięci, dopóki cykliczny GC nie zadziała. (Dlatego nigdy nie należy przechowywać danych śledzenia w zmiennej lokalnej ).

Jedyną rzeczą, o której mogę pomyśleć, byłaby dla ciebie globalna stuffmonkeypatch, więc kiedy myśli, że łapie Exception, w rzeczywistości łapie wyspecjalizowany typ, a wyjątek propaguje się do ciebie jako dzwoniącego:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()
ecatmur
źródło
7
To jest źle. Python 3 umieszcza obiekt traceback w wyjątku, jako e.__traceback__.
Glenn Maynard,
6
@GlennMaynard Python 3 rozwiązuje problem, usuwając cel wyjątku po wyjściu z exceptbloku, zgodnie z PEP 3110 .
ecatmur