python - 在 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,因为“流行”方法中的嵌套调用
解决方案
您面临的问题是因为类方法的装饰器不是传递的方法,而是函数。
在 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()
m
X().m
self
function
但是,您面临的问题是,仅当查找的值最终成为对象时才应用此魔术转换。如果它是实现类的实例__call__
(如您的情况),则不会发生包装,因此self
未绑定所需的值并且最终代码不起作用。
装饰器应该总是返回一个function
对象,而不是一个伪装成函数的类实例。请注意,您可以在装饰器中拥有所需的所有状态,因为function
Python 中的对象实际上是“闭包”,它们可以捕获可变状态。例如:
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 直接在类对象中查找它们。这是一个记录在案的实现选择,但它仍然是一个令人惊讶的“奇怪”不对称。
推荐阅读
- c# - 如何使可编写脚本的对象与 Unity 中的持久性数据一起使用
- postgresql - PostgreSql 查询缓存逻辑
- kubernetes - Kubernetes 中的对象、资源和控制器
- python - “NoneType”对象没有“大小”属性 - 如何使用 mtcnn 进行人脸检测?
- r - R:如果值满足特定条件,如何将数据框转换为邻接矩阵?
- php - 如何使用多个获取请求 .htaccess
- javascript - 如何保持底部工具提示与其元素的右端对齐?
- c# - ListView 克隆移除了默认字体功能
- php - 如何从数组的特定对象中获取 id?
- javascript - 如何使用 Express res.send() 发送数组