问题
魅影危机
假设我写了一个函数装饰器,它接收一个函数作为参数,并像下面这样用另一个函数包装它:
# 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__
属性允许链接异常。这个特性对我们来说不太有趣,因为它修改了异常而不是traceback。我们的目标是尽可能透明地包装异常,所以这样做行不通。
或者,我们可以尝试以下语法,承诺可以实现我们想要的效果(代码示例摘自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 ]
形式的语句,而是使用干净的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的行为几乎看起来像是一个设计缺陷,但我仍然认为应该有一种方法来处理这种情况。