如何从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
,那么将所有代码包装在执行异常打印本身的顶级函数中。 这不是也不应该很容易隐藏回溯的级别。
上一篇: How can I elide a function wrapper from the traceback in Python
下一篇: How to print the full traceback without halting the program?