如何从Python中的回溯中删除函数包装器

问题

魅影危机

说我写了一个函数装饰器,它接受函数,并将其包装在另一个函数中,如下所示:

# File example-1.py
from functools import wraps

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        return func(*args, **kwargs)
        # Do something
    # Do something
    return wrapper

现在让我们假设我正在装饰的函数引发一个异常:

@decorator
def foo():
    raise Exception('test')

运行foo()的结果将打印出以下回溯(在任何Python版本中):

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

克隆人的攻击

好的,现在我看看我的追踪,我发现它经历了wrapper功能。 如果我多次包装这个函数(大概是用一个稍微复杂的装饰器对象来接受其构造函数中的参数)呢? 如果我经常在我的代码中使用这个装饰器(我用它来进行日志记录,分析或其他)?

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

我不希望它“污染”我的回溯,当我从函数定义知道包装器在那里时,并且我不希望它多次显示它的代码片断时显示的是无用的return func(*args, **kwargs)

Python 2

西斯的复仇

在Python-2中,正如对不同问题的回答指出的那样,下面的技巧可以完成这项工作:

# In file example-2.py

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[0], info[1], info[2].tb_next
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

通过直接将调用与这个成语的包装函数包装在与我想从追溯中消除的函数相同的块中,我有效地从追踪中移除了当前层,并让异常继续传播。 每次堆栈展开都要经过这个函数,它将自己从回溯中移除,所以这个解决方案可以很好地工作:

Traceback (most recent call last):
  File "./example-2.py", line 28, in <module>
    foo()
  File "./example-2.py", line 26, in foo
    raise Exception('test')
Exception: test

(但是请注意,不能将这个习语封装在另一个函数中,因为一旦堆栈将从该函数退回到wrapper ,它仍将被添加到追踪中)

Python 3

新希望

现在我们已经介绍了这些,让我们继续讨论Python-3。 Python-3引入了这种新的语法:

raise_stmt ::=  "raise" [expression ["from" expression]]

它允许使用新异常的__cause__属性来链接异常。 这个特性对我们来说并不感兴趣,因为它修改了异常,而不是回溯。 我们的目标是成为一个完全透明的包装,只要知名度如此,所以这是不行的。

或者,我们可以尝试下面的语法,它承诺做我们想做的事(代码示例取自python文档):

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

使用这个语法我们可以尝试这样的事情:

# In file example-3
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[1].with_traceback(info[2].tb_next)
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

帝国反击战

但是,不幸的是,这不符合我们的要求:

Traceback (most recent call last):
  File "./example-3.py", line 29, in <module>
    foo()
  File "./example-3.py", line 17, in wrapper
    raise info[1].with_traceback(info[2].tb_next)
  File "./example-3.py", line 27, in foo
    raise Exception('test')
Exception: test

正如你所看到的那样,执行raise语句的行显示在回溯中。 这似乎来自这样一个事实:虽然Python-2语法设置从第三个参数开始追溯以在函数解开时raise ,因此它不会添加到追溯链中(如数据模型下的文档中所解释的) ,另一方面,Python-3语法将Exception对象的追踪更改为函数上下文中的表达式,然后将其传递给raise语句,该语句将代码中的新位置添加到追溯链中(对此的解释是在Python-3中非常相似)。

想到的解决方法是避免语句的"raise" [ expression ]形式,而是使用clean raise语句让异常像平常一样传播,但手动修改异常对象__traceback__属性:

# File example-4
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            info[1].__traceback__ = info[2].tb_next
            raise
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

但是这根本不起作用!

Traceback (most recent call last):
  File "./example-4.py", line 30, in <module>
    foo()
  File "./example-4.py", line 14, in wrapper
    return func(*args, **kwargs)
  File "./example-4.py", line 28, in foo
    raise Exception('test')
Exception: test

绝地的回归(?)

那么,我还能做什么? 看起来使用这种“传统”方式做这件事不会因为语法的改变而起作用,我不想在项目级开始搞乱回溯打印机制(使用traceback模块)。 这是因为如果不是不可能的话,如果不是不可能实现可扩展性,这不会破坏任何其他试图改变回溯的打包,以最高级别的自定义格式打印回溯,或者做其他任何事情与该问题有关。

另外,有人可以解释为什么实际上最后一种技术完全失败?

(我在python 2.6,2.7,3.4,3.6上试过这些例子)

编辑:经过一段时间的思考后,在我看来,python 3的行为更有意义,到python 2的行为几乎看起来像一个设计错误,但我仍然认为应该有办法做到这一点东东。


简单的答案是你不应该那样做。 从追踪中隐藏东西是危险的。 你可能认为你不想显示那行,因为它很简单或者“只是一个包装”,但是一般来说,如果它没有做任何事情,你就不会编写包装函数。 接下来你知道在包装函数中存在一个错误,现在这个错误是不可确定的,因为包装函数已经从回溯中清除了自身。

只需处理回溯中的多余行,或者,如果您真的想要,请覆盖sys.excepthook并将其过滤出顶层。 如果您担心其他人重写sys.excepthook ,那么将所有代码包装在执行异常打印本身的顶级函数中。 这不是也不应该很容易隐藏回溯的级别。

链接地址: http://www.djcxy.com/p/55187.html

上一篇: How can I elide a function wrapper from the traceback in Python

下一篇: How to print the full traceback without halting the program?