我正在使用装饰器来增强一些方法,但是它们之间缺乏互操作性。
举个例子,假设我想使用functools.cache
装饰器来记忆结果,并使用一个手工制作的装饰器来计算对该方法的调用次数:
from functools import cache, wraps
from typing import Callable
def counted(func: Callable) -> Callable:
@wraps(func)
def wrapped(*args,**kwargs):
setattr(wrapped, "calls", getattr(wrapped, "calls") + 1)
return func(*args,**kwargs)
setattr(wrapped, "calls", 0)
return wrapped
@counted
@cache
def func_a(data):
return data
if __name__ == "__main__":
func_a(1)
func_a.clear_cache()
print(func_a.calls)
如图所示,代码在func_a.clear_cache()
处失败,因为counted
装饰器没有传递cache
添加到函数中的方法/属性。如果我们交换两个装饰器,那么print(func_a.calls)
将失败,因为cache
装饰器没有传递由内部装饰器设置的属性calls
。
有没有一种Python方法可以得到一个最终的函数,其中包含装饰器添加的所有位?
我知道我可以修改counted
装饰器来显式传递cache
添加的属性,但是当您使用两个或更多第三方装饰器时,问题就出现了。
1条答案
按热度按时间roejwanj1#
装饰只是将某个可调用对象(函数或类)传递给另一个可调用对象(装饰器)的语法糖,并且该语法仅限于类/函数定义语句。
给定某个装饰器
dec
,编写等效于:
同样需要强调的是,装饰本身并没有什么特别之处。下面的例子是完全正确的(尽管不是很有用):
这表明,在装饰器的输出上留下某种“痕迹”的装饰并没有什么神奇之处。
Bar
类和g
函数基本上由dec
函数使用,并且由于dec
函数不返回对它们的引用,因此在此修饰之后,它们不再以任何方式可用。从装饰器返回 * 函数 * 本身也没有什么特别之处:
同样,装饰器只是一个函数,在这个例子中,它返回另一个函数,但是这个过程中没有对函数
g
做任何神奇的事情(或者任何事情)。在这个例子中,它在装饰之后基本上丢失了。为了更好地代表真实世界的场景,装饰器通常被编写为保留被装饰的可调用对象的某些信息,但这通常只是意味着 Package 器函数被定义在装饰器内部,并且在该 Package 器内部调用原始的可调用对象。
对原始函数
f
的引用并没有丢失,但是它位于dec
返回的wrapper
的本地命名空间中,并且无法再访问它。所有这一切都是为了让我们明白为什么没有神奇的内置方式来“保留”被装饰的可调用对象的任何属性。如果你想让装饰器做到这一点,你需要自己处理这一点。同样的,你也必须为任何其他将某个对象作为参数的函数编写这种逻辑。如果你期望这个对象的一些属性出现在这个函数的输出中,而如果你使用的是别人的函数,而他们 * 没有 * 这样做,那你就倒霉了。
functools.wraps
通过提供一个准标准的模式来编写decorator-wrappers来解决这个问题,它在 Package 器的__wrapped__
属性中保留了一个对被装饰对象的显式引用。但是没有任何东西 * 强迫 * 您使用这个模式,如果有人不这样做,那么您就倒霉了。你能做的最好的事情就是编写(另一个)定制装饰器,它 * 依赖于 * 其他装饰器使用
functools.wraps
(或functools.update_wrapper
)来递归地传播从 Package 对象链到顶层 Package 器的所有内容。它看起来像这样:但是同样,如果它下面的一个装饰器没有**(正确地)*在它返回的对象上设置
__wrapped__
属性,这将不起作用。这种方法当然允许额外的定制,比如告诉你的装饰器, 哪些 * 属性要从 Package 的对象中“拉”出来,或者 * 排除 * 哪些属性,或者是否用内部对象的属性覆盖后面的装饰器设置的属性等等。
假设您总是能够检查所使用的第三方装饰器的源代码,那么通过将其应用于正确使用
@wraps
-模式的装饰器,您至少可以通过这种方式获得一些您正在寻找的内容。