Python 使用类写装饰器的小技巧
本文介绍如何使用Python类实现装饰器。通过定义__call__方法,类实例可像函数一样被调用,从而实现缓存等装饰功能。文章进一步探讨了类装饰器在装饰类方法时遇到的“self”参数缺失问题,并提供了将可调用对象包装为函数或使用@property的两种解决方案,帮助开发者理解类装饰器的适用场景与限制。
最近发现了一种有趣的装饰器实现方式,特此记录分享。
装饰器的本质是一个返回函数的函数。除了常见的在函数内部定义内部函数的写法外,Python还支持使用类来定义装饰器。
1、用类写装饰器
下面展示一个使用传统函数方式实现的缓存装饰器示例。
def cache(func):
data = {}
def wrapper(*args, **kwargs):
key = f'{func.__name__}-{str(args)}-{str(kwargs)})'
if key in data:
result = data.get(key)
print('cached')
else:
result = func(*args, **kwargs)
data[key] = result
print('calculated')
return result
return wrapper
观察缓存功能的实际运行效果。
@cache def rectangle_area(length, width): return length * width rectangle_area(2, 3) # calculated # 6 rectangle_area(2, 3) # cached # 6
装饰器语法中的@cache相当于执行了func = cache(func)。如果这里的cache不是一个函数,而是一个类,情况会如何变化?假设定义一个名为Cache的类,执行func = Cache(func)后得到的是一个对象实例,此时返回的func实际上是Cache类的实例。通过定义__call__方法,可以将类的实例转化为可调用对象,使其行为类似于函数。在__call__方法内部调用原始的func函数即可实现装饰逻辑。因此,Cache类也可以作为装饰器使用,并支持@Cache语法。
接下来将之前的cache函数改写为Cache类实现:
class Cache:
def __init__(self, func):
self.func = func
self.data = {}
def __call__(self, *args, **kwargs):
func = self.func
data = self.data
key = f'{func.__name__}-{str(args)}-{str(kwargs)})'
if key in data:
result = data.get(key)
print('cached')
else:
result = func(*args, **kwargs)
data[key] = result
print('calculated')
return result
再次查看缓存结果,其表现与函数版完全一致。
@Cache def rectangle_area(length, width): return length * width rectangle_area(2, 3) # calculated # 6 rectangle_area(2, 3) # cached # 6
2、装饰类的方法
装饰器不仅用于普通函数,也常用于装饰类的方法。但我发现,使用类编写的装饰器不能直接应用于装饰类的方法(这听起来有些复杂……)。
先看看使用函数编写的装饰器是如何装饰类方法的。
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@cache
def area(self):
return self.length * self.width
r = Rectangle(2, 3)
r.area()
# calculated
# 6
r.area()
# cached
# 6
然而,如果直接替换为Cache类则会报错。错误原因在于,area方法被装饰后变成了类的一个属性,而不是绑定的方法。
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@Cache
def area(self):
return self.length * self.width
r = Rectangle(2, 3)
r.area()
# TypeError: area() missing 1 required positional argument: 'self'
Rectangle.area
# <__main__.Cache object at 0x0000012D8E7A6D30>
r.area
# <__main__.Cache object at 0x0000012D8E7A6D30>
回顾未使用装饰器的情况,Python在实例化对象后,会自动将函数转换为绑定方法。
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
Rectangle.area
# <function Rectangle.area at 0x0000012D8E7B28C8>
r = Rectangle(2, 3)
r.area
# <bound method Rectangle.area of <__main__.Rectangle object
因此,解决这个问题的方法很简单:若要用类编写的装饰器来装饰类的方法,只需将该可调用对象重新包装为普通函数即可。
# 定义一个简单的装饰器,什么也不做,仅仅是把可调用对象包装成函数
def method(call):
def wrapper(*args, **kwargs):
return call(*args, **kwargs)
return wrapper
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@method
@Cache
def area(self):
return self.length * self.width
r = Rectangle(2, 3)
r.area()
# calculated
# 6
r.area()
# cached
# 6
或者,使用@property装饰器可以直接将方法转换为属性访问。
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@property
@Cache
def area(self):
return self.length * self.width
r = Rectangle(2, 3)
r.area
# calculated
# 6
r.area
# cached
# 6
总结
使用类编写装饰器并非什么高深技巧,在大多数情况下确实没有必要这样做。不过,这种方式允许利用类的特性(如继承)来构建装饰器,提供了一种不同的实现思路。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













