Python 装饰器能否根据前一个装饰器的结果来执行或跳过?

2 投票
1 回答
1628 浏览
提问于 2025-05-01 16:34

我从这个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 个回答

2

我觉得你可能忽略了一个关键点:装饰器其实就是一个函数,它接收一个函数作为参数,然后返回另一个函数。

所以,这两个是一样的:

@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 为真,logsend_email 装饰器就会被应用;否则,会应用一个不改变函数的装饰器,它只是返回原函数。

但是如果你不能传递装饰器,因为函数已经被装饰过了怎么办?好吧,如果你定义每个装饰器时让它暴露出它所包裹的函数,你总是可以解开它。如果你总是使用 functools.wraps(如果没有特别理由,通常应该这样做——即使有理由,也可以很容易地模仿这种方式),被包裹的函数总是可以通过 __wrapped__ 访问。所以,你可以写一个装饰器,条件性地移除最外层的装饰:

def undecorate_if(cond):
    def decorate(f):
        return f.__unwrapped__ if cond else f
    return decorate

再说一次,如果你想动态地做到这一点,你可能会动态地进行装饰。所以,一个更简单的解决方案是,在应用之前,从 decos 可迭代对象中移除你不想要的装饰器。

撰写回答