Python 装饰器能否根据前一个装饰器的结果来执行或跳过?
我从这个Stack Overflow帖子了解到,在Python中,装饰器是按照它们在代码中出现的顺序来应用的。
那么下面这段代码应该怎么运行呢?
@unittest.skip("Something no longer supported")
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
def test_this():
....
第一个装饰器(下面有说明)要求测试运行器完全跳过test_this()
这个测试。
@unittest.skip("Something no longer supported")
而第二个装饰器则要求测试运行器在特定条件下跳过运行test_this()
。
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
这是不是意味着,除非我们把条件跳过的装饰器放在前面,否则test_this
根本不会被运行?
另外,在Python中有没有办法定义装饰器的依赖执行呢?比如:
@skipIf("Something goes wrong")
@skipIf(not something_feature_enabled, "Requires extra crunchy cookies to run")
@log
@send_email
def test_this():
....
这个想法是,如果@skipIf("Something goes wrong")
为true
,那么就启用@log
和@send_email
的执行。
如果我漏掉了什么很明显的东西,真是抱歉。
1 个回答
我觉得你可能忽略了一个关键点:装饰器其实就是一个函数,它接收一个函数作为参数,然后返回另一个函数。
所以,这两个是一样的:
@log
def test_this():
pass
def test_this():
pass
test_this = log(test_this)
同样的道理:
@skip("blah")
def test_this():
pass
def test_this():
pass
test_this = skip("blah")(test_this)
一旦你明白了这一点,你的所有问题都会变得简单很多。
首先,是的,skip(…)
是用来装饰 skipIf(…)(test)
的,所以如果它跳过了被装饰的东西,test
就永远不会被调用。
定义装饰器调用顺序的方法就是按照你希望的顺序来写它们。
如果你想动态地做到这一点,你可以在一开始就动态地应用装饰器。例如:
for deco in sorted_list_of_decorators:
test = deco(test)
另外,Python中有没有办法定义装饰器的依赖执行?
没有,它们都会被执行。更相关的是,每个装饰器是应用在被装饰的函数上,而不是装饰器本身。
但你总是可以把一个装饰器传递给一个条件装饰器:
def decorate_if(cond, deco):
return deco if cond else lambda f: f
然后:
@skipIf("Something goes wrong")
@decorate_if(something_feature_enabled, log)
@decorate_if(something_feature_enabled, send_email)
def test_this():
pass
简单吧?
现在,如果 something_feature_enabled
为真,log
和 send_email
装饰器就会被应用;否则,会应用一个不改变函数的装饰器,它只是返回原函数。
但是如果你不能传递装饰器,因为函数已经被装饰过了怎么办?好吧,如果你定义每个装饰器时让它暴露出它所包裹的函数,你总是可以解开它。如果你总是使用 functools.wraps
(如果没有特别理由,通常应该这样做——即使有理由,也可以很容易地模仿这种方式),被包裹的函数总是可以通过 __wrapped__
访问。所以,你可以写一个装饰器,条件性地移除最外层的装饰:
def undecorate_if(cond):
def decorate(f):
return f.__unwrapped__ if cond else f
return decorate
再说一次,如果你想动态地做到这一点,你可能会动态地进行装饰。所以,一个更简单的解决方案是,在应用之前,从 decos
可迭代对象中移除你不想要的装饰器。