使用缓存的计算属性

在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
上一篇:JS原型链模式和继承模式


下一篇:463.Island Perimeter