Python: 异常装饰器怎么保留堆栈追踪

20 投票
2 回答
11832 浏览
提问于 2025-04-17 11:04

我正在写一个装饰器,用来给一个函数加上功能。这个装饰器的作用是捕捉任何异常,然后根据原始异常的信息抛出一个自定义的异常。(这是因为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 个回答

5

我遇到了一个问题,就是我的自定义装饰器装饰的测试出现了问题。

为了在单元测试的输出中保留原始的跟踪信息,我在装饰器的主体中使用了以下的结构:

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

在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中,这个情况稍微有些变化。在Python 3里,追踪信息是和异常实例绑定在一起的,并且它有一个with_traceback的方法:

raise MyError(e).with_traceback(tb)

另一方面,Python 3还引入了异常链式调用的概念,这在很多情况下更有意义;要使用这个功能,你只需要:

raise MyError(e) from e

撰写回答