阅读flask的源码,发现了一个有趣的装饰器,locked_cached_property, 根据其文档描述,这个装饰器有着神奇的功能:
代码实现,并不复杂,去除掉函数文档以后内容如下
from threading import RLock
_missing = object()
class locked_cached_property(object):
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func
self.lock = RLock()
def __get__(self, obj, type=None):
if obj is None:
return self
with self.lock:
value = obj.__dict__.get(self.__name__, _missing)
if value is _missing:
value = self.func(obj)
obj.__dict__[self.__name__] = value
return value
# 以下是使用示例
class CachePropertyTest():
def __init__(self, length, width):
self.length = length
self.width = width
@locked_cached_property
def area(self):
print('calc area')
return self.length * self.width
cpt = CachePropertyTest(3, 4)
print(cpt.area)
print(cpt.area)
area原本是CachePropertyTest里的一个方法,但是被装饰以后,就可以像使用属性一样去使用它,这是装饰器的第一个功能。
我两次调用cpt.area,但print('calc area') 只被执行了一次,只有第一次调用时执行了方法area里的代码,之后返回的都是缓存的结果,这是装饰器的第二个功能。
装饰器的第三个功能,无需测试验证,阅读其代码,__get__方法中, 使用RLock来对获取属性值的过程进行加锁操作,确保即使是在多线程条件下,方法area也只会被执行一次。
理解这个装饰器及其作用的关键是理解描述器。Python 有三个特殊方法,__get__、__set__、__delete__,用于覆盖属性的一些默认行为,如果一个类定义了其中一个方法,那么它的实例就是描述器。
方法area被装饰以后,area就不再是方法,而是一个locked_cached_property 的实例,也就是一个描述器,下面的代码可以证明
print(type(CachePropertyTest.area)) # <class '__main__.locked_cached_property'>
print(isinstance(CachePropertyTest.area, locked_cached_property)) # True
描述器是一种代理机制,对属性的操作由这个描述器来代理,如何理解这句话呢?
当代码执行到print(cpt.area), 正常情况下,是要到cpt对象的 __dict__中寻找area属性,代码表示为cpt.__dict__['area'],如果找不到,则执行type(cpt).__dict__['x'], 去CachePropertyTest的__dict__中继续寻找,如果还找不到,则去CachePropertyTest的父类里继续寻找,不包括元类。
如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法,具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。
于是cpt.area就变成了descr.__get__(self, obj, type=None)这种形式,根据上面的示例,代码可以表示为
print(cpt.area)
print(locked_cached_property.__get__(CachePropertyTest.area, cpt))
这两行代码输出相同的结果。
在上面的例子中,我想根据长宽来计算面积,但我不想写一个get_area方法,因为这个面积我会经常用到,我实在不想每次都进行一次计算。同时,我也不想事先把面积计算出来,然后把面积作为一个属性存储起来,因为有可能我在某次运行过程中,一次面积也用不到,那么提前计算就浪费时间了。
用到的时候,再去计算,且只计算一次,这种情况下就适合用惰性计算。
举一个更贴合实际的例子,加入你的系统运行期间,需要请求一些第三方API接口,这些接口是要收费的,那么现在,你不能在程序启动后就把这些接口事先请求一遍,这样太浪费钱了。运行期间,用到哪个接口的数据了,你再去发请求,获得数据,运行期间,可能要反复的用这个接口的数据,但后续的使用就无需再发请求了,只需要使用第一次请求后的数据就可以了。
惰性计算,随用随得,且只计算一次,不仅如此,还帮你做了缓存,省力方便。
QQ交流群: 211426309