Извлечь информацию о трассировке из объекта исключения

Для объекта исключения (неизвестного происхождения) существует ли способ получить трассировку? У меня такой код:

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

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

Как я могу извлечь трассировку из объекта Exception, когда она есть?

Ответ 1

Ответ на этот вопрос зависит от версии Python, которую вы используете.

В Python 3

Все просто: исключения снабжены атрибутом __traceback__, который содержит трассировку. Этот атрибут также доступен для записи, и его можно легко установить с помощью метода исключений with_traceback:

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

Эти функции минимально описаны как часть raise документации.

Вся ответственность за эту часть ответа должна идти на Vyctor, который первым опубликовал эту информацию. Я включил его сюда только потому, что этот ответ застрял вверху, а Python 3 становится все более распространенным.

В Python 2

Это досадно сложно. Проблема с трассировками заключается в том, что у них есть ссылки на стековые фреймы, а у стековых фреймов есть ссылки на трассировки, которые имеют ссылки на стековые фреймы , которые имеют ссылки на.... Это вызывает проблемы для сборщика мусора. (Спасибо ecatmur за первое упоминание об этом.)

Хорошим способом решения этой проблемы было бы хирургическое прерывание цикла после выхода из предложения except, что и делает Python 3. Решение Python 2 намного уродливее: вам предоставляется специальная функцияsys.exc_info(), которая работает только внутри предложения except. Он возвращает кортеж, содержащий исключение, тип исключения и обратную трассировку для любого исключения, которое обрабатывается в данный момент.

Поэтому, если вы находитесь в предложении except, вы можете использовать выходные данные sys.exc_info() вместе с модулем traceback для выполнения различных полезных действий:

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

Но, как показывает ваше редактирование, вы пытаетесь получить трассировку, которая была бы напечатана, если бы ваше исключение не было обработано, после того, как оно уже было обработано. Это гораздо более сложный вопрос. К сожалению, sys.exc_info возвращает (None, None, None), когда не обрабатывается исключение. Другие связанные атрибуты sys тоже не помогают. sys.exc_traceback устарела и не определена, когда исключение не обрабатывается; sys.last_traceback кажется идеальным, но, по-видимому, он определяется только во время интерактивных сеансов.

Если вы можете контролировать возникновение исключения, вы можете использовать inspect и пользовательское исключение для хранения некоторой информации. Но я не совсем уверен, как это будет работать.

Честно говоря, ловить и возвращать исключение - довольно необычная вещь. Это может быть признаком того, что вам все равно нужно провести рефакторинг.

Ответ 2

Так как Python 3.0 [PEP 3109] встроенный класс Exception имеет атрибут __traceback__, который содержит traceback object (с Python 3.2.3):

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

Проблема заключается в том, что после Googling __traceback__ какое-то время я нашел только несколько статей, но ни один из них не описывает, нужно ли вам (почему) не) использовать __traceback__.

Однако документация Python 3 для raise говорит, что:

Объект трассировки обычно создается автоматически, когда возникает исключение и прикрепляется к нему как атрибут __traceback__, который доступен для записи.

Поэтому я предполагаю, что он предназначен для использования.

Ответ 3

Способ получения трассировки в виде строки из объекта исключения в 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(...) возвращает список строк. ''.join(...) объединяет их. Для получения дополнительной информации, пожалуйста, посетите: https://docs.python.org/3/library/traceback.html#traceback.format_tb

Ответ 4

Кроме того, если вы действительно хотите получить полную трассировку, как вы видели бы, чтобы она была напечатана на вашем терминале, вам нужно это:

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

Если вы используете format_tb, как указано выше, ответы предполагают, что вы получите меньше информации:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>

Ответ 5

Существует очень веская причина, по которой трассировка не сохраняется в исключении; поскольку трассировка содержит ссылки на локальные стеки, это приведет к циклической ссылке и (временной) утечке памяти до тех пор, пока не начнет работать циклический GC. (Вот почему вы не должны никогда не сохранять трассировку в локальной переменной.)

Единственное, о чем я могу подумать, это о том, чтобы вы monkeypatch stuff глобализовали так, чтобы, когда он думает, что он ловит Exception, он фактически ловит специализированный тип и исключение распространяется на вас как вызывающего:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()