什么是描述符(descriptor)

By liuzhijun, 2014-02-26, 分类: 默认

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之禅」(id:vttalk)获取最新文章 python之禅