the5fire的技术博客

关注python、vim、linux、web开发和互联网--life is short, we need python.


Python的Descriptor在Django中的使用

作者:the5fire | 标签:       | 发布:2014-05-15 4:21 p.m. | 阅读量: 4863, 4785

这篇通过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这个关键字的应用场景:

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的需求。因此这么定义:

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这样的东西了。

这个东西的原理就是,既然你已经计算完了,那么就把它的结果直接塞到你的实例对象中去吧。它的代码是这样的:

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

然后我们再使用这个描述器来实现我们上面的需求:

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


----EOF-----

扫码关注,或者搜索微信公众号:码农悟凡


其他分类: