探析cached_propety装饰器缓存的原理

一 背景

今天在用FastDfs重写Django默认的Storage存储系统,跟着源码来设计一些常用的功能,正好看到了一个装饰器cached_propety,看着这个名字,非常好懂,缓存+描述器。于是我打算学习下源码中是如何利用描述器实现缓存的。

cached_property是我在Django的Storage类中找到的,其他地方也可以找到。


二 源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class cached_property:
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.

A cached property can be made out of an existing method:
(e.g. ``url = cached_property(get_absolute_url)``).
On Python < 3.6, the optional ``name`` argument must be provided, e.g.
``url = cached_property(get_absolute_url, name='url')``.
"""
name = None

@staticmethod
def func(instance):
raise TypeError(
'Cannot use cached_property instance without calling '
'__set_name__() on it.'
)

@staticmethod
def _is_mangled(name):
return name.startswith('__') and not name.endswith('__')

def __init__(self, func, name=None):
if PY36:
self.real_func = func
else:
func_name = func.__name__
name = name or func_name
if not (isinstance(name, str) and name.isidentifier()):
raise ValueError(
"%r can't be used as the name of a cached_property." % name,
)
if self._is_mangled(name):
raise ValueError(
'cached_property does not work with mangled methods on '
'Python < 3.6 without the appropriate `name` argument. See '
'https://docs.djangoproject.com/en/%s/ref/utils/'
'#cached-property-mangled-name' % get_docs_version(),
)
self.name = name
self.func = func
self.__doc__ = getattr(func, '__doc__')

def __set_name__(self, owner, name):
if self.name is None:
self.name = name
self.func = self.real_func
elif name != self.name:
raise TypeError(
"Cannot assign the same cached_property to two different names "
"(%r and %r)." % (self.name, name)
)

def __get__(self, instance, cls=None):
"""
Call the function and put the return value in instance.__dict__ so that
subsequent attribute access on the instance returns the cached value
instead of calling cached_property.__get__().
"""
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res

这个装饰器的源码不算长,这里主要关注的几个函数分别为__init____set_name___,__get__方法,我之前认为的类装饰器一般都会使用__call__内置方法,在这里我要直呼装饰器的强大。

之前还看见DRF的viewset中action装饰器工厂,在增强被修饰的函数其实装饰器工厂只需两层就够了,并不一定像书上说的装饰器工厂需要三层,三层的情况一般在装饰器中显示调用了被修饰的函数,这里可能您看着比较绕,我会在之后的笔记中详细解读各类框架中的优秀装饰器!


1.首先我们来看下init()初始化函数,传入了两个额外参数,分别为func,name,func其实就是装饰器修饰的方法,而name默认为None,为可变防御参数。init()方法主要是获取被修饰函数的一些元属性,包括__name__,然后进行类型检查等,并规定被cached_property修饰的方法不能是魔法方法(内置方法),原因异常中给出了,小于python3.6的版本没有name属性。

注:这里有一点我没有弄懂,他在这里加了name,那我该如何在写@cached_property的时候添加上name呢?如果作为@cached_property的参数,则会报参数不够的异常。如果有大佬知道,欢迎给我留言或者注册下,还请您不吝珠玉!


2.接着来看第二个函数__get__()函数,熟悉描述器的高手们肯定知道这个这个方法是属于描述器协议中的。用于当一个定义了__get__()/__set__()/__del__()的协议的类作为另一个类的类属性时,调用该属性,则会调用__get__()方法。我在另一篇笔记中讲解了描述器。这个函数是整个缓存思想的核心哦,原理就是首先调用被修饰的方法,然后获取到结果,将其保存在调用类(不是cached_propery,而是其所修饰的函数所在的类)的实例对象的实例字典__dict__中,键为默认为函数名。然后返回结果。

注:第一次会调用get()方法,因为__dict__中没有对应的键值对,而第一次调用后会添加键值对,以便后面的调用都会直接获取实例字典中的属性,而不需要重复调用被修饰的函数了。这样有效的利用实例字典缓存数据提高了性能!

为此我自己模仿它,写了简单的类并通过Debug来验证其只有在第一此调用才会调用get(),之后则会调用getattribute()方法,实则调用__dict[name]__.__get__(self,type(self))


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Test(object):

counts = 0

def __get__(self, instance, cls=None):
self.counts += 1
instance.__dict__['t'] = 'ok!'
print(self.counts)
return instance.__dict__['t']

class Operation(object):

t = Test()

m = Operation()

print(m.t) # 第一次调用__get()__方法

print(m.t) # 第二次调用__getattribute__()<==>__dict__['t'].__get__(m,type(m))

感兴趣的可以利用debug __get__()来看counts的打印次数,只有一次


3.最后第三个函数,就是__set_name__函数,一边运行时可以动态修改name值,修改dict字典中映射键值对。


总结

看完上面的讲解,是不是发现大神们的框架中有很多高级的用法呢?或许肯定有小伙伴觉得麻烦,认为直接在函数中可以直接利用实例字典进行缓存。是的,没错,但是框架毕竟是框架,要高可用和扩展性强才能证明是一个好框架。而利用了装饰器的解耦效果,增强了扩展性!