Python: декоратор исключений. Как сохранить трассировку стека - PullRequest
19 голосов
/ 25 января 2012

Я пишу декоратор для применения к функции.Он должен перехватить любое исключение, а затем вызвать пользовательское исключение на основе исходного сообщения об исключении.(Это потому, что suds генерирует общее исключение WebFault, из сообщения которого я анализирую исключение, генерируемое веб-службой, и вызываю исключение Python для его зеркалирования.)

Однако, когда я вызываю пользовательское исключение в оболочкеЯ хочу, чтобы трассировка стека указывала на функцию, которая вызвала исходное исключение WebFault.То, что у меня есть, вызывает правильное исключение (оно динамически анализирует сообщение и создает экземпляр класса исключения). Мой вопрос: Как сохранить трассировку стека для указания на исходную функцию, вызвавшую исключение WebFault?

from functools import wraps

def try_except(fn):
        def wrapped(*args, **kwargs):
            try:
                fn(*args, **kwargs)
            except Exception, e:
                parser = exceptions.ExceptionParser()
                raised_exception = parser.get_raised_exception_class_name(e)
                exception = getattr(exceptions, raised_exception)
                raise exception(parser.get_message(e))
        return wraps(fn)(wrapped)

Ответы [ 2 ]

41 голосов
/ 25 января 2012

В Python 2.x малоизвестная особенность raise заключается в том, что его можно использовать более чем с одним аргументом: форма с тремя аргументами raise принимает тип исключения, экземпляр исключения ипроследить.Вы можете получить при трассировке sys.exc_info(), которая возвращает (не случайно) тип исключения, экземпляр исключения и трассировку.

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

Итак:

import sys

class MyError(Exception):
    pass

def try_except(fn):
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except Exception, e:
            et, ei, tb = sys.exc_info()
            raise MyError, MyError(e), tb
    return wrapped

def bottom():
   1 / 0

@try_except
def middle():
   bottom()

def top():
   middle()

>>> top()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "tmp.py", line 24, in top
    middle()
  File "tmp.py", line 10, in wrapped
    return fn(*args, **kwargs)
  File "tmp.py", line 21, in middle
    bottom()
  File "tmp.py", line 17, in bottom
    1 / 0
__main__.MyError: integer division or modulo by zero

В Python 3 это немного изменилось.Там вместо этого трассировки присоединяются к экземпляру исключения, и у них есть метод with_traceback:

raise MyError(e).with_traceback(tb)

С другой стороны, Python 3 также имеет исключение цепочка , что делает болеесмысл во многих случаях;чтобы использовать это, вы просто используете:

raise MyError(e) from e
5 голосов
/ 12 сентября 2012

Я столкнулся с этой проблемой в тестах, которые были украшены моими пользовательскими декораторами.

Я использовал следующую конструкцию в теле декоратора для сохранения оригинальной трассы, напечатанной в выходных данных юнит-тестов:

try:
    result = func(self, *args, **kwargs)
except Exception:
    exc_type, exc_instance, exc_traceback = sys.exc_info()
    formatted_traceback = ''.join(traceback.format_tb(
        exc_traceback))
    message = '\n{0}\n{1}:\n{2}'.format(
        formatted_traceback,
        exc_type.__name__,
        exc_instance.message
    )
    raise exc_type(message)
...