传递属性的Python装饰器

v440hwme  于 2022-10-30  发布在  Python
关注(0)|答案(1)|浏览(129)

我正在使用装饰器来增强一些方法,但是它们之间缺乏互操作性。
举个例子,假设我想使用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添加的属性,但是当您使用两个或更多第三方装饰器时,问题就出现了。

roejwanj

roejwanj1#

装饰只是将某个可调用对象(函数或类)传递给另一个可调用对象(装饰器)的语法糖,并且该语法仅限于类/函数定义语句。
给定某个装饰器dec,编写

@dec
def f(): ...

等效于:

def f(): ...

f = dec(f)

同样需要强调的是,装饰本身并没有什么特别之处。下面的例子是完全正确的(尽管不是很有用):

def dec(_): return 1

class Foo: pass

@dec
class Bar: pass

def f(): pass

@dec
def g(): pass

print(Foo)  # <class '__main__.Foo'>
print(Bar)  # 1
print(f)    # <function f at 0x7fdf...>
print(g)    # 1

这表明,在装饰器的输出上留下某种“痕迹”的装饰并没有什么神奇之处。
Bar类和g函数基本上由dec函数使用,并且由于dec函数不返回对它们的引用,因此在此修饰之后,它们不再以任何方式可用。
从装饰器返回 * 函数 * 本身也没有什么特别之处:

def f():
    return "There is no spoon"

def dec(_func):
    return f

@dec
def g():
    return "Hi mom"

print(g.__name__)  # f
print(g())         # There is no spoon

同样,装饰器只是一个函数,在这个例子中,它返回另一个函数,但是这个过程中没有对函数g做任何神奇的事情(或者任何事情)。在这个例子中,它在装饰之后基本上丢失了。
为了更好地代表真实世界的场景,装饰器通常被编写为保留被装饰的可调用对象的某些信息,但这通常只是意味着 Package 器函数被定义在装饰器内部,并且在该 Package 器内部调用原始的可调用对象。

def dec(func):
    def wrapper():
        return func() + " There is no spoon."
    return wrapper

@dec
def f():
    return "Hi mom."

print(f.__name__)  # wrapper
print(f())         # Hi mom. There is no spoon.

对原始函数f的引用并没有丢失,但是它位于dec返回的wrapper的本地命名空间中,并且无法再访问它。
所有这一切都是为了让我们明白为什么没有神奇的内置方式来“保留”被装饰的可调用对象的任何属性。如果你想让装饰器做到这一点,你需要自己处理这一点。同样的,你也必须为任何其他将某个对象作为参数的函数编写这种逻辑。如果你期望这个对象的一些属性出现在这个函数的输出中,而如果你使用的是别人的函数,而他们 * 没有 * 这样做,那你就倒霉了。
functools.wraps通过提供一个准标准的模式来编写decorator-wrappers来解决这个问题,它在 Package 器的__wrapped__属性中保留了一个对被装饰对象的显式引用。但是没有任何东西 * 强迫 * 您使用这个模式,如果有人不这样做,那么您就倒霉了。
你能做的最好的事情就是编写(另一个)定制装饰器,它 * 依赖于 * 其他装饰器使用functools.wraps(或functools.update_wrapper)来递归地传播从 Package 对象链到顶层 Package 器的所有内容。它看起来像这样:

from functools import wraps

def propagate_all_attributes(func):
    wrapped = getattr(func, "__wrapped__", None)
    if wrapped is not None:
        propagate_all_attributes(wrapped)
        # Add attributes from `wrapped` that are *not* present in `func`:
        for attr_name, attr_value in wrapped.__dict__.items():
            if attr_name not in func.__dict__:
                func.__dict__[attr_name] = attr_value
    return func

def dec1(func):
    @wraps(func)
    def wrapper():
        return func() + " There is no spoon."
    wrapper.x = 1
    wrapper.y = 2
    return wrapper

def dec2(func):
    @wraps(func)
    def wrapper():
        return func() + " Have a cookie."
    wrapper.y = 42
    return wrapper

@propagate_all_attributes
@dec2
@dec1
def f():
    """Some function"""
    return "Hi mom."

print(f.__name__)  # f
print(f.__doc__)   # Some function
print(f.x)         # 1
print(f.y)         # 42
print(f())         # Hi mom. There is no spoon. Have a cookie.

但是同样,如果它下面的一个装饰器没有**(正确地)*在它返回的对象上设置__wrapped__属性,这将不起作用。
这种方法当然允许额外的定制,比如告诉你的装饰器,
哪些 * 属性要从 Package 的对象中“拉”出来,或者 * 排除 * 哪些属性,或者是否用内部对象的属性覆盖后面的装饰器设置的属性等等。
假设您总是能够检查所使用的第三方装饰器的源代码,那么通过将其应用于正确使用@wraps-模式的装饰器,您至少可以通过这种方式获得一些您正在寻找的内容。

相关问题