首页 > 解决方案 > 在 Python 中使用函数或类实现装饰器时的不同行为

问题描述

我想编写一个装饰器以应用于类的方法。装饰器应该保持一个状态,因此我想用一个类来实现它。但是,当有嵌套调用时,类装饰器会失败,而使用函数构建的装饰器可以正常工作。

这是一个简单的例子:

def decorator(method):
    def inner(ref, *args, **kwargs):
        print(f'do something with {method.__name__} from class {ref.__class__}')
        return method(ref, *args, **kwargs)

    return inner


class class_decorator:

    def __init__(self, method):
        self.method = method

    def __call__(self, *args, **kwargs):
        print('before')
        result = self.method(*args, **kwargs)
        print('after')
        return result


class test:

    #@decorator
    @class_decorator
    def pip(self, a):
        return a + 1

    #@decorator
    @class_decorator
    def pop(self, a):
        result = a + self.pip(a)
        return result

t = test()
    
print(f'result pip : {t.pip(3)}')
print(f'result pop : {t.pop(3)}')

这将适用于“装饰器”功能,但不适用于 class_decorator,因为“流行”方法中的嵌套调用

标签: pythondecorator

解决方案


您面临的问题是因为类方法的装饰器不是传递的方法,而是函数。

在 Python 中,方法和函数是两种不同的类型:

Python 3.8.3 (default, May 17 2020, 18:15:42)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.15.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: class X:
   ...:     def m(self, *args, **kwargs):
   ...:         return [self, args, kwargs]

In [2]: type(X.m)
Out[2]: function

In [3]: type(X().m)
Out[3]: method

In [4]: X.m(1,2,x=3)
Out[4]: [1, (2,), {'x': 3}]

In [5]: X().m(1,2,x=3)
Out[5]: [<__main__.X at 0x7f1424f33a00>, (1, 2), {'x': 3}]

当在实例中查找时,会发生从函数(如m中所示X)到方法(在实例中查找时变成的样子)的“神奇”转换。在实例本身中找不到 Python 在类中查找它,但是当它发现它是一个函数时,返回给请求者的值被“包装”在包含该值的方法对象中。X()mX().mself

function但是,您面临的问题是,仅当查找的值最终成为对象时才应用此魔术转换。如果它是实现类的实例__call__(如您的情况),则不会发生包装,因此self未绑定所需的值并且最终代码不起作用。

装饰器应该总是返回一个function对象,而不是一个伪装成函数的类实例。请注意,您可以在装饰器中拥有所需的所有状态,因为functionPython 中的对象实际上是“闭包”,它们可以捕获可变状态。例如:

In [1]: def deco(f):
   ...:     state = [0]
   ...:     def decorated(*args, **kwargs):
   ...:         state[0] += 1
   ...:         print(state[0], ": decorated called with", args, **kwargs)
   ...:         res = f(*args, **kwargs)
   ...:         print("return value", res)
   ...:         return res
   ...:     return decorated

In [2]: class X:
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:     @deco
   ...:     def a(self):
   ...:         return self.x + 1
   ...:     @deco
   ...:     def b(self):
   ...:         return 10 + self.a()

In [3]: x = X(12)

In [4]: x.a()
1 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
Out[4]: 13

In [5]: x.a()
2 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
Out[5]: 13

In [6]: x.b()
1 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
3 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
return value 23
Out[6]: 23

在上面我使用了一个简单的列表state,但你可以使用尽可能多的状态,包括类实例。然而,重要的一点是装饰器返回的是一个function对象。这样,当在类实例中查找时,Python 运行时将构建正确的method对象以使方法调用起作用。

另一个需要考虑的非常重要的一点是,装饰器是在类定义时(即构建类对象时)而不是在实例创建时执行的。这意味着您将在装饰器中拥有的状态将在类的所有实例之间共享。

另一个可能不明显并且在过去困扰我的事实是,特殊方法喜欢__call____add__不首先在实例中查找,Python 直接在类对象中查找它们。这是一个记录在案的实现选择,但它仍然是一个令人惊讶的“奇怪”不对称。


推荐阅读