描述器(1)-原理说明

简介

python的描述器称为descriptor。其作用和它的名字一样,可以理解为用于描述一个属性,描述可以理解称为一些操作。而实际上描述器是一个属性对象,因此这里可以认为是被描述的属性。从实现上讲,是实现了描述器协议的对象,描述器协议即实现了__get__,__set__,__delete__函数,具体定义如下:

1
2
3
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None

这里实现了__get__和__set__的描述器是称为资料描述器,仅定义了__get__的是非资料描述器。

示例

如果要对某一类属性进行处理,比如学生成绩单,每一科的成绩都不能为负,设定其大于等于0。
正常情况下我们可能需要对每一属性设置set,这时候可以使用描述器来简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class score_desc(object):
def __set__(self, obj, score):
print 'call __set__'
if score >= 0:
self.score = score
else:
raise Exception("score<0!")

def __get__(self, obj, obj_type=None):
print 'call __get__'
return self.score


class stu_scores(object):
math_score = score_desc()
chinese_score = score_desc()


if __name__ == '__main__':
sc = stu_scores()
sc.math_score = 10
print sc.math_score

输出:

1
2
3
call __set__
call __get__
10

这里我们对每一个属性都可以做这种设置,所以描述器也可以理解称为带功能的属性。

属性访问

描述器是通过__getattribute__调用的,分为object.__getattribute__和type.__getattribute__两种,分别对实例对属性的查找和类对属性的查找。

实例对属性的查找

对于obj.m这种形式的调用,该函数是用C实现,在Object/object.c中的PyObject_GenericGetAttr函数实现,通过阅读源码可以知道查找过程如下:ect_GenericGetAttr函数实现,通过阅读源码可以知道查找过程如下:
1、首先是获取通过type(obj).__mro__给出的类元组,其包括当前obj的类和其继承的类,依次搜索元组中的__dict__中名称为m的属性,找到了就赋值给descr的变量。
2、然后判断descr是否是一个资料描述器,如果是则执行descr.__get__(obj,type(obj)),返回结果。
3、如果不满足第2点,然后移动obj指针指向obj实例的__dict__,如果找到m,则返回结果。这里需要注意,如果m是一个描述器,也是直接返回了这个描述器,而不是调用__get__函数。
比如上面的stu_socres类改写成下面这样:

1
2
3
4
class stu_scores(object):
def __init__(self):
self.math_score = score_desc()
self.chinese_score = score_desc()

输出就是:

1
10

所以描述器都是做为类属性这一级别。
4、如果不满足第3点,则继续判断descr是否是非资料描述器,如果是则执行其__get__函数,返回结果。
5、如果不满足第4点,则判断第1点中搜到的descr是否为空,不为空则返回descr
6、如果前面的都不满足,则抛出异常。

类对属性的查找

上面是对于实例访问属性的情况,如果对象是一个类的时候即cls.m的时候,通过Objects/typeobject.c中的type_getattro__函数实现。过程和实例的实现有点不同:
1、首先也是通过type(cls).__mro__给出元类元组,一般是(<type ‘type’>, <type ‘object’>)这样的形式。然后也是依次在元组中的类的__dict__中搜索m,并赋值给meta_attribute。
2、然后判断meta_attribute是否是资料描述器,如果是则返回meta_attribute.__get__(None,cls)执行结果。
3、如果不满足第2点,然后通过cls.__mro__给出类元组,然后依次查找。将查找结果赋值给attribute.
4、然后判断attribute是否是描述器(实现了__get__),如果是则返回attribute.__get__(None,cls)执行结果。
5、如果不满足第4点,则继续判断meta_attribute是否是非资料描述器(实现了__get__),如果有执行,返回结果。
6、如果不满足第5点,判断meta_attribute是否为空,即普通的属性,如果不是则返回该值。
7、如果上面不满足,则抛出异常。

访问优先级

从上面可以看出,一般实例obj的属性x(x不是描述器的情况)获取顺序是:
1、实例的dict,即obj.__dict__[‘x’];
2、类的dict,即type(obj).__dict__[‘x’];
3、父类的dict,不包括元类metaclass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test(object):
def __init__(self):
self.test = 'instance test'
self.t = 'intance t'

def test(self):
print 'class function test'

t = 'class t'

if __name__ == '__main__':
t = Test()
print t.t
print t.test

输出:

1
2
intance t
instance test

如果去掉init中的内容,输出为:

1
2
class t
<bound method Test.test of <__main__.Test object at 0x7f06476a4210>>

如果上面的查找过程中找到某个x是一个描述器,则用x定义的描述器方法来替代默认的操作访问。优先级是资料描述器>实例字典>非资料描述器。比如实例的方法和属性同名,会优先使用属性,因为实例函数只是一个非资料描述器。
通过一下代码输出test函数信息:

1
2
print type(t.test)
print type(t.test).__dict__['__get__']

输出:

1
2
<type 'instancemethod'>
<slot wrapper '__get__' of 'instancemethod' objects>

属性赋值查找

赋值的时候过程是相同的,对应的是Objects/typeobject.c的type_setattro方法,其中调用了Objects/object.c的PyObject_GenericSetAtt方法。其查找顺序是:
1、type(obj)或者type(cls)的mro类元组中类的__dict__中查找,找到属性且为数据描述器,调用其__set__函数,返回。
2、如果不满足上一步,则继续查找obj或者cls的的__dict__查找,如果找到则赋值返回。由此可知,对于类继承的属性这里是没有办法直接赋值修改的。

1
2
3
4
5
6
7
8
9
10
11
class A(object):
a = 10

class B(A):
b = 10

print B.a
print B.__dict__
B.a=20
print B.__dict__
print B.a

输出:

1
2
3
4
10
{'__module__': '__main__', 'b': 10, '__doc__': None}
{'a': 20, '__module__': '__main__', 'b': 10, '__doc__': None}
20

这里可以看到B对a赋值会直接在字典中加了新的a属性
3、如果不满足上一步,则继续判断第一步是否找到属性切实现了__set__方法,如果有则执行后返回。
4、以上都不满足,抛出异常。