如何从Python-3中的traceback中删除函数包装器?

Re.*_*.po 10 python decorator python-2.x traceback python-3.x

问题

魅影危机

假设我写了一个函数装饰器来获取该函数,并将其包装在另一个函数中,如下所示:

# 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
Run Code Online (Sandbox Code Playgroud)

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

@decorator
def foo():
    raise Exception('test')
Run Code Online (Sandbox Code Playgroud)

运行的结果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
Run Code Online (Sandbox Code Playgroud)

克隆人的攻击

好的,现在我看看我的追溯,我看到它通过该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
Run Code Online (Sandbox Code Playgroud)

当我从函数定义中知道包装器在那里时,我不希望它"污染"我的回溯,并且当它显示的代码片段没有帮助时我不希望它多次出现 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
Run Code Online (Sandbox Code Playgroud)

通过将这个习惯用与我想要从回溯中删除的函数相同的块直接包装到包装函数的调用,我有效地从回溯中删除当前层并让异常继续传播.每次堆栈展开都经过这个函数时,它会从回溯中移除它,所以这个解决方案完美地工作:

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
Run Code Online (Sandbox Code Playgroud)

(但请注意,您无法将此成语封装在另一个函数中,因为一旦堆栈将从该函数中退出wrapper,它仍将被添加到追溯中)

Python 3

新希望

现在我们已经涵盖了这一点,让我们继续前进到Python-3.Python-3引入了这种新语法:

raise_stmt ::=  "raise" [expression ["from" expression]]
Run Code Online (Sandbox Code Playgroud)

它允许使用__cause__新异常的属性链接异常.这个功能对我们来说无趣,因为它修改了异常,而不是回溯.我们的目标是成为一个完全透明的包装器,就可见性而言,这是不可能的.

或者,我们可以尝试以下语法,它承诺执行我们想要的操作(从python文档中获取代码示例):

raise Exception("foo occurred").with_traceback(tracebackobj)
Run Code Online (Sandbox Code Playgroud)

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

# 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
Run Code Online (Sandbox Code Playgroud)

帝国反击战

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

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
Run Code Online (Sandbox Code Playgroud)

如您所见,执行raise语句的行显示在回溯中.这似乎来自这样一个事实:虽然Python-2语法将回溯从第三个参数设置raise为函数被展开,因此它没有被添加到回溯链中(如数据模型中的文档中所述),另一方面,Python-3语法将Exception对象上的回溯更改为函数上下文中的表达式,然后将其传递给raise语句,该语句将代码中的新位置添加到回溯链中(对此的解释非常相似)蟒-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
Run Code Online (Sandbox Code Playgroud)

但这根本不起作用!

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
Run Code Online (Sandbox Code Playgroud)

绝地归来(?)

那么,我还能做什么?似乎使用"传统"这样做的方法因为语法的改变而无法工作,我不想traceback在项目级别开始搞乱使用回溯打印机制(使用模块).这是因为如果不是不可能在可扩展中实现它将是困难的,这对于尝试更改回溯的任何其他包,在顶层以自定义格式打印回溯,或者以其他方式执行任何其他操作都不会中断与此问题有关.

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

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

编辑:思考了一段时间,在我看来,以后的巨蟒-3行为更有意义,给蟒蛇2行为几乎看起来像一个设计错误的观点,但我仍然认为,应该有一个办法还挺做到这一点东西.

Bre*_*arn 6

简单的答案是你不应该这样做。从回溯中隐藏东西是危险的。您可能认为您不想显示该行,因为它是微不足道的或“只是一个包装器”,但一般来说,如果它不执行某些操作,您就不会编写包装器函数。接下来你知道包装函数中有一个错误,现在无法找到,因为包装函数已经从回溯中删除了自己。

只需处理回溯中的额外行,或者,如果您真的想要,sys.excepthook在顶层覆盖并过滤掉它们。如果您也担心其他人会覆盖sys.excepthook,那么将您的所有代码包装在一个顶级函数中,该函数会自行打印异常。从回溯中隐藏级别不是也不应该容易。