深入解析Python中的descriptor描述器的作用及用法
一般来说,一个描述器是一个有“绑定行为”的对象属性(objectattribute),它的访问控制被描述器协议方法重写。这些方法是__get__(),__set__(),和__delete__()。有这些方法的对象叫做描述器。
默认对属性的访问控制是从对象的字典里面(__dict__)中获取(get),设置(set)和删除(delete)它。举例来说,a.x的查找顺序是,a.__dict__['x'],然后type(a).__dict__['x'],然后找type(a)的父类(不包括元类(metaclass)).如果查找到的值是一个描述器,Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意,只有在新式类中时描述器才会起作用。(新式类是继承自type或者object的类)
描述器是强大的,应用广泛的。描述器正是属性,实例方法,静态方法,类方法和super的背后的实现机制。描述器在Python自身中广泛使用,以实现Python2.2中引入的新式类。描述器简化了底层的C代码,并为Python的日常编程提供了一套灵活的新工具。
描述器协议
descr.__get__(self,obj,type=None)-->value descr.__get__(self,obj,value)-->None descr.__delete__(self,obj)-->None
一个对象如果是一个描述器,被当做对象属性(很重要)时重写默认的查找行为。
如果一个对象同时定义了__get__和__set__,它叫datadescriptor。仅定义了__get__的描述器叫non-datadescriptor。
datadescriptor和non-datadescriptor区别在于:相对于实例的字典的优先级,如果实例字典有与描述器具同名的属性,如果描述器是datadescriptor,优先使用datadescriptor。如果是non-datadescriptor,优先使用字典中的属性。
classB(object): def__init__(self): self.name='mink' def__get__(self,obj,objtype=None): returnself.name classA(object): name=B() a=A() printa.__dict__#print{} printa.name#printmink a.name='kk' printa.__dict__#print{'name':'kk'} printa.name#printkk
这里B是一个non-datadescriptor所以当a.name='kk'的时候,a.__dict__里会有name属性,接下来给它设置__set__
def__set__(self,obj,value): self.name=value ...dosomething a=A() printa.__dict__#print{} printa.name#printmink a.name='kk' printa.__dict__#print{} printa.name#printkk
因为datadescriptor访问属性优先级比实例的字典高,所以a.__dict__是空的。
描述器的调用
描述器可以直接这么调用:d.__get__(obj)
然而更常见的情况是描述器在属性访问时被自动调用。举例来说,obj.d会在obj的字典中找d,如果d定义了__get__方法,那么d.__get__(obj)会依据下面的优先规则被调用。
调用的细节取决于obj是一个类还是一个实例。另外,描述器只对于新式对象和新式类才起作用。继承于object的类叫做新式类。
对于对象来讲,方法object.__getattribute__()把b.x变成type(b).__dict__['x'].__get__(b,type(b))。具体实现是依据这样的优先顺序:资料描述器优先于实例变量,实例变量优先于非资料描述器,__getattr__()方法(如果对象中包含的话)具有最低的优先级。完整的C语言实现可以在Objects/object.c中PyObject_GenericGetAttr()查看。
对于类来讲,方法type.__getattribute__()把B.x变成B.__dict__['x'].__get__(None,B)。用Python来描述就是:
def__getattribute__(self,key): "Emulatetype_getattro()inObjects/typeobject.c" v=object.__getattribute__(self,key) ifhasattr(v,'__get__'): returnv.__get__(None,self) returnv
其中重要的几点:
- 描述器的调用是因为__getattribute__()
- 重写__getattribute__()方法会阻止正常的描述器调用
- __getattribute__()只对新式类的实例可用
- object.__getattribute__()和type.__getattribute__()对__get__()的调用不一样
- 资料描述器总是比实例字典优先。
- 非资料描述器可能被实例字典重写。(非资料描述器不如实例字典优先)
- super()返回的对象同样有一个定制的__getattribute__()方法用来调用描述器。调用super(B,obj).m()时会先在obj.__class__.__mro__中查找与B紧邻的基类A,然后返回A.__dict__['m'].__get__(obj,A)。如果不是描述器,原样返回m。如果实例字典中找不到m,会回溯继续调用object.__getattribute__()查找。(译者注:即在__mro__中的下一个基类中查找)
注意:在Python2.2中,如果m是一个描述器,super(B,obj).m()只会调用方法__get__()。在Python2.3中,非资料描述器(除非是个旧式类)也会被调用。super_getattro()的实现细节在:Objects/typeobject.c,[del]一个等价的Python实现在Guido'sTutorial[/del](译者注:原文此句已删除,保留供大家参考)。
以上展示了描述器的机理是在object,type,和super的__getattribute__()方法中实现的。由object派生出的类自动的继承这个机理,或者它们有个有类似机理的元类。同样,可以重写类的__getattribute__()方法来关闭这个类的描述器行为。
描述器例子
下面的代码中定义了一个资料描述器,每次get和set都会打印一条消息。重写__getattribute__()是另一个可以使所有属性拥有这个行为的方法。但是,描述器在监视特定属性的时候是很有用的。
classRevealAccess(object): """Adatadescriptorthatsetsandreturnsvalues normallyandprintsamessageloggingtheiraccess. """ def__init__(self,initval=None,name='var'): self.val=initval self.name=name def__get__(self,obj,objtype): print'Retrieving',self.name returnself.val def__set__(self,obj,val): print'Updating',self.name self.val=val >>>classMyClass(object): x=RevealAccess(10,'var"x"') y=5 >>>m=MyClass() >>>m.x Retrievingvar"x" 10 >>>m.x=20 Updatingvar"x" >>>m.x Retrievingvar"x" 20 >>>m.y 5
这个协议非常简单,并且提供了令人激动的可能。一些用途实在是太普遍以致于它们被打包成独立的函数。像属性(property),方法(bound和unboundmethod),静态方法和类方法都是基于描述器协议的。