使用类作为装饰器?

时间:2021-02-05 12:57:39

标签: python

我认为以下内容可以用作装饰器

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)


class A:
    @D
    def f(self, x):
        pass

a=A()
a.f(1)

但我得到 TypeError: f() missing 1 required positional argument: 'x'

这是怎么回事,有没有办法像这样使用类作为装饰器?

1 个答案:

答案 0 :(得分:1)

问题是除了装饰器机制之外,还有 Python 使用的机制,以便类体内的函数表现为实例方法:它是 "descriptor protocol"。这实际上很简单:所有函数对象都有一个 __get__ 方法(但不是 __set____del__)方法,这使它们成为“非数据描述符”。当 Python 从实例中检索属性时,__get__ 会以实例作为参数被调用 - __get__ 方法必须返回一个作为方法工作的可调用对象,并且必须知道哪个是实例调用:


# example only - DO NOT DO THIS but for learning purposes,
#  due to concurrency problems:

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        
        return self.func(self.instance, *args, **kwargs)

    def __get__(self, instance, owner):
        self.instance = instance
        return self

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)

这将打印“<ma​​in.A object at 0x...> 1”

但是,由于很容易察觉,这仅允许一次在单个实例中调用装饰方法 - 即使拥有多个“A”实例的非并行代码也可能使用错误的实例调用该方法头脑。也就是说,这个序列:


In [127]: a1 = A()                

In [128]: a2 = A()                

In [129]: f1 =  a1.f              

In [130]: f2 = a2.f               

In [131]: f1() 

最终会调用“a2.f()”而不是“a1.f()”

为了避免这种情况,您必须返回一个来自 __get__ 的可调用对象,它不需要将实例作为类属性进行检索。一种方法是创建一个 partial 可调用并包含它 - 但是,请注意,由于这是一个必要步骤,因此装饰器类本身不需要具有“运行包装器 + 原始代码”功能在 __call__ 方法中 - 它可以有任何名称:

from functools import partial

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, _instance=None, **kwargs):
        if _instance:
            return self.func(_instance, *args, **kwargs)
        else: 
            return self.func(*args, **kwargs)
        

    def __get__(self, instance, owner):
        return partial(self.__call__, _instance=instance)
      

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)