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

时间:2017-06-28 22:09:27

标签: python python-3.x decorator python-2.x traceback

问题

幽灵威胁

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

# 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

我不想要它&#34;污染&#34;当我从函数定义中知道包装器在那里时我的追溯,并且当它显示的代码片段没有帮助时我不希望它多次出现return func(*args, **kwargs)

Python 2

西斯的复仇

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

# 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__属性链接异常。这个功能对我们来说无趣,因为它修改了异常,而不是回溯。我们的目标是成为一个完全透明的包装器,就可见性而言,所以不会这样做。

或者,我们可以尝试以下语法,promises to do我们想要什么(代码示例来自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

返回绝地(?)

那么,我还能做什么?看起来好像正在使用&#34;传统&#34;这样做的方式因为语法的改变而无法工作,我不想在项目级别开始搞乱跟踪打印机制(使用traceback模块)。这是因为如果不是不可能在可扩展中实现它会很难实现,这对于尝试更改回溯的任何其他包不会造成破坏,在顶层以自定义格式打印回溯,或者否则做与此问题有关的任何其他事情。

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

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

编辑:在考虑了一段时间之后,在我看来python 3的行为更有意义,以至于python 2行为几乎看起来像一个设计bug,但我仍然认为应该有办法做这件事。

1 个答案:

答案 0 :(得分:3)

简单的答案是你不应该这样做。从追溯中隐藏东西是危险的。您可能认为自己并不想显示该行,因为它只是一个小问题或者只是一个包装器#34;但是一般来说,如果它没有&#编写包装函数,你就不会这样做#&#34; 39;做点什么。接下来你知道包装器函数中有一个错误,现在它是不可用的,因为包装函数已经从回溯中删除了。

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