回溯显示在装饰器之前

12 投票
1 回答
2682 浏览
提问于 2025-04-17 13:41

这个很不错的小工具是用Python写的装饰器,它可以让你选择性地禁用被装饰的函数:

enabled = get_bool_from_config()

def run_if_enabled(fn):
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs) if enabled else None
        except Exception:
            log.exception('')
            return None
    return wrapped

可惜的是,如果在fn()函数内部出现了错误,错误信息只会显示到包装器的部分:

Traceback (most recent call last):
  File "C:\my_proj\run.py", line 46, in wrapped
    return fn(*args, **kwargs) if enabled else None
  File "C:\my_proj\run.py", line 490, in a_decorated_function
    some_dict['some_value']
KeyError: 'some_value'
  1. 为什么会这样?
  2. 有没有办法让我们看到完整的错误信息?

1 个回答

8

啊!好的,这个问题挺有意思的!

这里有一个差不多的函数,它直接从 sys.exc_info() 中获取异常:

import sys
import traceback

def save_if_allowed(fn):
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs) if enabled else None
        except Exception:
            print "The exception:"
            print "".join(traceback.format_exception(*sys.exc_info()))
            return None
    return wrapped

@save_if_allowed
def stuff():
    raise Exception("stuff")


def foo():
    stuff()

foo()

没错:打印出来的错误追踪信息中没有更高层的调用信息:

$ python test.py
The exception:
Traceback (most recent call last):
  File "x.py", line 21, in wrapped
    return fn(*args, **kwargs) if enabled else None
  File "x.py", line 29, in stuff
    raise Exception("stuff")
Exception: stuff

现在,为了更清楚一点,我猜这是因为错误追踪信息只包含到最近的 try/except 块的堆栈信息……所以我们可以在不使用装饰器的情况下重现这个情况:

$ cat test.py
def inner():
    raise Exception("inner")

def outer():
    try:
        inner()
    except Exception:
        print "".join(traceback.format_exception(*sys.exc_info()))

def caller():
    outer()

caller()

$ python test.py
Traceback (most recent call last):
  File "x.py", line 42, in outer
    inner()
  File "x.py", line 38, in inner
    raise Exception("inner")
Exception: inner

啊哈!现在想想,这在某种程度上是有道理的:此时,异常 只遇到了两个堆栈帧:一个是 inner() 的,另一个是 outer() 的——异常 还不知道 outer() 是从哪里被调用的。

所以,要获取完整的堆栈信息,你需要把当前的堆栈和异常的堆栈结合起来:

$ cat test.py
def inner():
    raise Exception("inner")

def outer():
    try:
        inner()
    except Exception:
        exc_info = sys.exc_info()
        stack = traceback.extract_stack()
        tb = traceback.extract_tb(exc_info[2])
        full_tb = stack[:-1] + tb
        exc_line = traceback.format_exception_only(*exc_info[:2])
        print "Traceback (most recent call last):"
        print "".join(traceback.format_list(full_tb)),
        print "".join(exc_line)

def caller():
    outer()

caller()

$ python test.py
Traceback (most recent call last):
  File "test.py", line 56, in <module>
    caller()
  File "test.py", line 54, in caller
    outer()
  File "test.py", line 42, in outer
    inner()
  File "test.py", line 38, in inner
    raise Exception("inner")
Exception: inner

另请参见:

撰写回答