在Python中,将方法变为属性使用@property的装饰器。有时候,为了提高性能,想在仅首次调用方法property时进行计算,后续则使用缓存的值。
此时,可以使用一个类装饰器,如下:
class LazyProperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls):
if instance is None:
return self
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
关于__get__, __set__, __delete__等方法的使用,可以参考描述符的参考文章,内容较多。
可以按如下方式使用:
import math
class Circle:
def __init__(self, radius):
this.radius = radius
@LazyProperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@LazyProperty
def perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
>>>
可以看出,在首次调用c.area或c.perimeter时,会使用LazyProperty的__get__方法,而在第二次及之后的调用中,直接得到了计算的值。原因是使用@LazyProperty时,area和perimeter此时变为描述符对象,在进行属性访问时,若定义了描述符类的__get__, __set__, __delete__时,会分别调用各自的方法。调用顺序和是否仅定义__get__有关。若仅定义__get__,则为非数据描述符(non-data descriptor),若定义了__set__或__delete__,则为数据描述符(data descriptor)。详细文档可参考Implementing Descriptors。
在第一次调用area和perimeter时,会计算实际的值,然后调用setattr方法将函数名和值存入instance的__dict__中。下次调用时,又因为仅定义了__get__方法,没哟家定义__set__和__delete__,故该描述符仅为非数据描述符。非数据描述符在属性访问时,会被对象的底层字典属性所覆盖。
The important points to remember are:
- descriptors are invoked by the __getattribute__() method
- overriding __getattribute__() prevents automatic descriptor calls
- object.__getattribute__() and type.__getattribute__() make different calls to __get__().
- data descriptors always override instance dictionaries.
- non-data descriptors may be overridden by instance dictionaries.
该实现方案的一个潜在缺陷是,若进行如下操作,则可以修改已计算的值,造成数据失真:
>>> c.area
Computing area
50.26548245743669
>>> c.area = 25
>>> c.area
25
>>>
有人可能会说,可以给LazyProperty添加__set__方法,方法内部直接抛出raise AttributeError("Cannot change the value")。但添加__set__的同时,会改变属性的访问优先级,此时缓存失效。调用area和perimeter时,每次都是调用底层的__get__,每次都会计算值。
此时,可以使用方法装饰器,如下:
def lazy_property(func):
name = '_lazy_' + func.__name__
@property
def lazy(self):
if hasattr(self, name):
return getattr(self, name)
value = func(self)
setattr(self, name, value)
return value
return lazy
class Circle:
def __init__(self, radius):
this.radius = radius
@lazy_property
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@lazy_property
def perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius