什么是描述符(descriptor)

By 刘志军 , 2014-02-26, 分类: python

descriptor

只要是定义了__get__()__set()____delete()__中任意一个方法的对象都叫描述符。那描述符协议是什么呢?这个协议指的就是这三个方法。

    descr.__get__(self, obj, type=None) --> value

    descr.__set__(self, obj, value) --> None

    descr.__delete__(self, obj) --> None

那么描述符有什么牛逼的? 通常来说Python对象的属性控制默认是这样的:从对象的字典(__dict__)中获取(get),设置(set),删除(delete),比如:对于实例aa.x的查找顺序为a.__dict__['x'],然后是type(a).__dict__['x'].如果还是没找到就往上级(父类)中查找。描述符就好比是破坏小子,他会改变这种默认的控制行为。究竟是怎么改变的呢?

想必会你已经猜到了,如果属性x是一个描述符,那么访问a.x时不再从字典__dict__中读取,而是调用描述符的__get__()方法,对于设置和删除也是同样的原理。

既然知道他有化腐朽为神奇的这种特点,聪明的你一定能想到的能用在什么场景下,我用邮件地址的验证这个简单的例子来演示他是如何运作的。

    class Person(object):
        def __init__(self, email):
            self.email = email

现在如果有不安分的小子总想着搞破坏,传递一个无效的email过来,如果你不使用描述符你是没辙的,你别告诉我说你可以在init方法里面做验证嘛?老兄,python是一门动态语言,也没有像我大java一样拥有私有变量。用一个例子来粉碎你的猜想。

    import re
    class Person(object):
        def __init__(self, email):
            m = re.match('\w+@\w+\.\w+', email)
            if not m:
                raise Exception('email not valid')
            self.email = email

上面这个初始化方法看似完美有缺,如果客户端能安分的按规则行房,错了,是行事。就不会出什么大问题。传入的无效值也能优雅的以异常的形式警告。

    >>> p = test.Person('lzjun567@gmail.com')
    >>> p.email
    'lzjun567@gmail.com'
    >>> p2 = test.Person('dfsdfsdf')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "test.py", line 38, in __init__
        raise Exception('email not valid')
    Exception: email not valid
    >>> 

但是,捣蛋小子来了,他要这样给p对象赋值email:

    >>> p.email = 'sdfsdfsdf'
    >>> p.email
    'sdfsdfsdf'
    >>> p.__dict__
    {'email': 'sdfsdfsdf'}
    >>> 

这时的p.email默认从__dict__读取值。你看给p传个火星来的email地址也能接受。这下只有上帝能救你于水火之中,其实上帝就是那个描述符啦。那怎么把email变成一个描述符啊?当然方式有好几种:

基于类创建描述符

    import re

    class Email(object):

        def __init__(self):
            self._name = ''

        def __get__(self, obj, type=None):
            return self._name

        def __set__(self, obj, value):
            m = re.match('\w+@\w+\.\w+', value)
            if not m:
                raise Exception('email not valid')
            self._name = value

        def __delete__(self, obj):
            del self._name

    class Person(object):
        email = Email()

这下你给他赋值一个火星文看看:

    >>> p = Person()
    >>> p.email = 'ではないああを行う'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "test.py", line 46, in __set__
        raise Exception('email not valid')
    Exception: email not valid
    >>> 

    >>> p.email = 'lzjun@gmail.com'
    >>> p.email
    'lzjun@gmail.com'

现在总算是能抵挡住大和民族的呀咩嗲了,再来看看__dict__中有哪些东西:

    >>> Person.__dict__
    dict_proxy({'__dict__': <attribute '__dict__' of 'Person' objects>, '__module__': 'test', '__weakref__': <attribute '__weakref__' of 'Person' objects>, 'email': <test.Email object at 0x8842fcc>, '__doc__': None})
    >>> p.__dict__
    {}

嗯,纵使email赫然在列dict中,拥有了描述符后,解释器对其视而不见,转而去调用描述符中对应的方法。即使是下面的操作方式也是徒劳而已:

    >>> p.__dict__['email'] = 'xxxxxx'
    >>> p.email
    'lzjun@gmail.com'
    >>> 

使用property()函数创建描述符

    class Person(object):

        def __init__(self):
            self._email = None

        def get_email(self):
            return self._email

        def set_email(self, value):
             m = re.match('\w+@\w+\.\w+', value)
             if not m:
                 raise Exception('email not valid')
             self._email = value

        def del_email(self):
            del self._email

        email = property(get_email, set_email, del_email, 'this is email property')


    >>> p = Person()
    >>> p.email
    >>> p.email = 'dsfsfsd'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "test.py", line 71, in set_email
        raise Exception('email not valid')
    Exception: email not valid
    >>> p.email = 'lzjun567@gmail.com'
    >>> p.email
    'lzjun567@gmail.com'
    >>> 

property()函数返回的是一个描述符对象,它可接收四个参数:property(fget=None, fset=None, fdel=None, doc=None)

采用property实现描述符与使用类实现描述符的作用是一样的,只是实现方式不一样。property的一种纯python的实现方式如下:

    class Property(object):
        "Emulate PyProperty_Type() in Objects/descrobject.c"

        def __init__(self, fget=None, fset=None, fdel=None, doc=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
            if doc is None and fget is not None:
                doc = fget.__doc__
            self.__doc__ = doc

        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError("unreadable attribute")
            return self.fget(obj)

        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError("can't set attribute")
            self.fset(obj, value)

        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError("can't delete attribute")
            self.fdel(obj)

        def getter(self, fget):
            return type(self)(fget, self.fset, self.fdel, self.__doc__)

        def setter(self, fset):
            return type(self)(self.fget, fset, self.fdel, self.__doc__)

        def deleter(self, fdel):
            return type(self)(self.fget, self.fset, fdel, self.__doc__)

留心的你发现property里面还有getter,setter,deleter方法,那他们是做什么用的呢?来看看第三种创建描述符的方法。

使用@property装饰器

    class Person(object):

        def __init__(self):
            self._email = None

        @property
        def email(self):
            return self._email

        @email.setter
        def email(self, value):
             m = re.match('\w+@\w+\.\w+', value)
             if not m:
                 raise Exception('email not valid')
             self._email = value

        @email.deleter
        def email(self):
            del self._email

    >>>
    >>> Person.email
    <property object at 0x02214930>
    >>> p.email = 'lzjun'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "test.py", line 93, in email
        raise Exception('email not valid')
    Exception: email not valid
    >>> p.email = 'lzjun@gmail.com'
    >>> p.email
    'lzjun@gmail.com'
    >>>

发现没有,其实装饰器property只是property函数的一种语法糖而已,setter和deleter作用在函数上面作为装饰器使用。

哪些场景用到了描述符

其实python的实例方法就是一个描述符,来看下面代码块:

    >>> class Foo(object):
    ...     def my_function(self):
    ...        pass
    ...
    >>> Foo.my_function
    <unbound method Foo.my_function>
    >>> Foo.__dict__['my_function']
    <function my_function at 0x02217830>
    >>> Foo.__dict__['my_function'].__get__(None, Foo)
    <unbound method Foo.my_function>
    >>> Foo().my_function
    <bound method Foo.my_function of <__main__.Foo object at 0x0221FFD0>>
    >>> Foo.__dict__['my_function'].__get__(Foo(), Foo)
    <bound method Foo.my_function of <__main__.Foo object at 0x02226350>>

my_function函数实现了__get__方法。描述符也被大量用在各种框架中,比如:django的paginator.py模块,django的model其实也使用了描述符。


关注公众号「Python之禅」,回复「1024」免费获取Python资源

python之禅