这篇通过Django源码中的cached_property来看下Python中一个很重要的概念——Descriptor(描述器)的使用。想必通过实际代码来看能让人对其用法更有体会。
什么是Descriptor?
Descriptor是Python中定义的一个协议,协议的内容是只要你定义的这个类(对象)具有: __get__, __set__, __delete__
方法中的任意一个你这个类(对象)就叫做Descriptor。那么Descriptor是做什么用的呢?简单来说它是用来拦截属性访问的。简单说法你不能理解的话,下面这句话应该能理解::
Descriptors are a powerful, general purpose protocol. They are the mechanism behind properties, methods, static methods, class methods, and super()。
翻译:Descriptor是强大且通用的协议。它是Python中的属性,方法,静态访问,类方法和super关键字的实现机理。
你可以打开你的shell,随便定义一个方法func,然后查看它有哪些属性: dir(func)
,会发现上面说的那个 __get__
。
更多的协议的细节可以看文章最后给出的链接。下面来看下这个Descriptor在Django中是怎么被使用的。
Django中的cached_property
在Django项目的utils/functional.py中这么一个类:cached_property。从名字上可以看出,它的作用是属性缓存。在看这个之前,先来看下在python中property这个关键字的应用场景:
.. code:: python
import datetime
class User(object):
birth_year = 1988
@property
def age(self):
return datetime.date.today().year - self.birth_year
这种使用场景很常见,我定义了一个属性(birth_year),但是又需要另外一个依赖于此属性的属性。再重复定义一个性质一样的字段显然冗余了,因此可以通过property来实现。上面的这个类实例化之后: user = User()
,可通过: user.age
直接获取到内容,而不是 user.age()
。其中的property就是一个描述器。拦截了对age的访问,把方法变成了属性。
在接触cached_property的代码之前,咱先自己实现了这个property,上面已经知道只需要定义 __get__, __set__, __delete__
其中一个。这里明显是get的需求。因此这么定义:
.. code:: python
import datetime
class my_property(object):
def __init__(self, func):
self.func = func # 保存原来的age方法
def __get__(self, instance, klass=None):
print instance, klass
return self.func(instance) # 调用原方法
class User(object):
birth_year = 1988
@my_property
def age(self):
return datetime.date.today().year - self.birth_year
user = User()
print user.age
其中关于@这个装饰器语法糖的使用,只需要理解如果一个方法(func)上加了@deco,就相当于是定义了这么个方法: deco(func)。除了装饰器可能有疑惑,其他的都比较好理解。
cached_property代码
理解了上面的例子在来看Django中的这个cached_property代码就容易多了。上面的property虽然是成功了添加了一个age的属性,但是每次调用这个属性都得再次计算,如果方法中的计算量比较大或者执行操作比较复杂的话,那效率岂不是很慢。因此就需要有cached这样的东西了。
这个东西的原理就是,既然你已经计算完了,那么就把它的结果直接塞到你的实例对象中去吧。它的代码是这样的:
.. code:: python
class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
"""
def __init__(self, func):
self.func = func
def __get__(self, instance, type=None):
print instance
if instance is None:
return self
# 关键点,直接通过内置的__dict__把属性塞回去。
res = instance.__dict__[self.func.__name__] = self.func(instance)
return res
然后我们再使用这个描述器来实现我们上面的需求:
.. code:: python
import datetime
class User(object):
birth_year = 1988
@cached_property
def age(self):
return datetime.date.today().year - self.birth_year
user = User()
print user.age
print user.age
print user.age
执行之后,会发现只会执行一次上面的那个print instance操作。这意味着只调用了一次原age方法。这里需要注意dict这个东西,在调用实例的属性时会先去这里面找,如果没找到就会去父类的dict中查找,如果还是没有,则会调用定义的属性,如果这个属性被描述器拦截了,则这个属性的行为就会被重写。
总结
上面仅仅是对get的简单应用。关于这个Descriptor更详细的介绍推荐大家看看官方文档或者翻译的文档。
参考:
http://pyzh.readthedocs.org/en/latest/Descriptor-HOW-TO-Guide.html https://docs.python.org/2/howto/descriptor.html
- from the5fire.com微信公众号:Python程序员杂谈