只要是定义了__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),比如:对于实例a
,a.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)
- fget:属性获取方法
- fset:属性设置方法
- fdel:属性删除方法
- doc: docstring
采用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资源