【发布时间】:2017-12-02 11:00:06
【问题描述】:
问题
幽灵的威胁
假设我写了一个函数装饰器,它接受该函数,并将其包装在另一个函数中,如下所示:
# 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
当我从函数定义中知道包装器存在时,我不希望它“污染”我的回溯,并且当它显示的代码 sn-p 无用时,我不希望它多次显示@ 987654329@
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,因为函数正在展开,因此它没有添加到回溯链中(如数据下的文档中所述Model),另一方面,Python-3 语法将Exception 对象上的回溯更改为函数上下文中的表达式,然后将其传递给raise 语句,该语句将代码中的新位置添加到回溯链(在 Python-3 中对此的解释非常相似)。
想到的解决方法是避免使用"raise" [ expression ] 形式的语句,而是使用干净的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 的行为几乎看起来像一个设计错误,但我仍然认为应该有办法做这种事。
【问题讨论】:
-
不完全有资格理解这个问题,但 +1 的努力:)
-
谢谢! ^_^ 我已经跟踪这个问题大约一周了。
标签: python python-3.x decorator python-2.x traceback