1 摘要
介绍python属性查找的相关基础知识,通过底层代码的分析,详细描述了python中对象属性的查找/赋值过程以及在object和type中对描述器的不同处理。并结合这些知识,简单探寻了一下描述器在python实现中的作用。
2 准备
在详解python属性查找之前,我们先来了解一些相关的概念,以便于理解整个查找过程。
2.1 描述器
描述器(descriptor )是一个有 “绑定行为” 的对象属性(object attribute),它的访问控制被描述器协议方法重写。描述器协议的方法为:
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找行为。
如果一个对象同时定义了 __get__() 和 __set__() ,它叫做资料描述器(data descriptor)。仅定义了 __get__() 的描述器叫非资料描述器(non-data descriptor, 常用于方法,当然其他用途也是可以的)。
资料描述器和非资料描述器的区别在于:相对于实例的字典的优先级。 如果实例字典中有与描述器同名的属性,如果描述器是资料描述器,优先使用资料描述器,如果是非资料描述器,优先使用字典中的属性。后面内容会具体介绍。
描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和 super 背后的实现机制(详细的解释请点击这里)。描述器在 Python 自身中广泛使用,用以实现 Python2.2 中引入的新式类。描述器简化了底层的C代码,并为 Python 的日常编程提供了一套灵活的新工具。
2.2 Python 方法解析顺序
在支持多重继承的编程语言中,查找方法具体来自那个类时的基类搜索顺序通常被称为方法解析顺序(Method Resolution Order),简称 MRO。Python中查找属性也遵循 MRO。对于仅支持单重继承的语言,MRO非常简单,只需要按着继承链搜索就行。但是在支持多继承的语言中,情况就比较复杂。“菱形继承” 的问题在 python 引入 “新式类” 后更加突出。
Python的历史版本中先后出现三种不同的MRO:经典方式、Python2.2 新式算法、Python2.3 新式算法(也称作C3)。Python 3中只保留了最后一种,即C3算法。C3算法最早被提出是用于Lisp的,在 python 中采用主要是为了解决之前之前基于深度优先搜索算法不满足本地优先级和单调性的问题:
- 本地优先级: 指声明时父类的顺序,比如C(A,B),在访问C类对象属性时,根据声明顺序应该先查找A类再查找B类。
- 单调性: 指如果在解析C类继承顺序时,A类排在B类的前面,那么在C类所有的子类里也必须满足这个顺序。
从 Python2.2 开始为解决引入新类所带来的方法解析顺序,采用的方案是在类定义时就计算出它的MRO ,并存储为该类对象的一个属性 __mro__ 。然后在查找属性时按照 __mro__ ** 依次搜索基类。
2.3 metaclass
在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。Python中的类还远不止如此。在python中类同样也是一种对象,只要你使用关键字class,解释器在执行的时候就会创建一个对象。例如:
1 | class ObjectCreator(object):pass |
上面的代码会在python解释器执行的时候生成一个名叫 ObjectCreator 的对象。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。 创建 “类” 的 “类” 就是元类(metaclass),python默认的内建元类是type。
1 | object.__class__ |
在 python 中通过 type(obj) ,或者 obj.__class__ 来获取对象的类型(type)。这里有个特别的 type 的 type 是 type。
1 | type(object) |
换句话说,元类(metaclass)的实例化是类(class),类(class)的实例化是类实例对象(object)。
注:type 这个类型(内建函数)根据传入的参数不同具有两个不同的功能,这可能是出于向后兼容的原因。
- type(object) -> the object’s type,返回的是 object 实例的类型。
- type(name, bases, dict) -> a new type,返回的是一个新类型。
3 属性的查找过程
为了描述方便,我们假定现在要访问 b.x。x 不是 python 内建的特殊属性。(注:如果查找的属性是一个 python 内建的特殊属性,直接就找到,不需要执行下面的过程!)
在 python 中属性的查找由类型的内部方法 __getattribute__() 支持。由于当 b 为object 或者 class 时,__getattribute__() 对于描述器的调用方式略有不同,为了更准确地描述一些细节,我们将查找过程按照 b 为 object 与 class (为了与内建元类 type 区别,这里不用 type 而用 class 代替)时进行分开描述(cpython 中实际对应的底层c代码实现也不同,具体细节后面会介绍)。
3.1 object属性的查找过程:object.__getattribute__()
沿着 type(b).__mro__ 搜索类的 __dict__ 中名称为 x 的属性,并将其值赋值给 descr 变量(descr默认为null);
若 descr 是一个 data descriptor 则执行
descr.__get__(b, type(b))
,并将执行结果返回,结束查找,否则进入下一步;在 b.__dict__ 中查找名称为 x 的属性,若找到则将其返回,结束查找,否则进入下一步;
若上述第2步查找失败(descr == null),则抛出 AttributeError 异常,结束查找。若descr(descr != null)是 non-data descriptor 则执行
descr.__get__(b, type(b))
,并将执行结果返回,结束查找;否则直接返回 descr, 结束查找。
cpython 对应的底层实现代码: PyObject_GenericGetAttr()
in Objects/object.c。
1 | PyObject * |
3.2 class属性的查找过程:type.__getattribute__()
沿着 type(b).__mro__ 搜索(元类)基类 __dict__ 中名称为 x 的属性,并将其值赋值给 meta_atrribute 变量(meta_atrribute默认为null);
若 meta_atrribute 是一个 data descriptor 则执行
meta_atrribute.__get__(b, type(b))
,并将执行结果返回,结束查找,否则进入下一步;在 b.__dict__ 中查找名称为 x 的属性,若没有找到,则进入下一步。若找到,当属性为 descriptor 时执行
meta_atrribute.__get__(None, b)
(这与 object 中不同,object 是直接将找到的 x 返回,而不会去执行 __get__ ), 并将其结果返回,否则直接将属性返回,结束查找。若上述第2步查找失败(即没有在元类中找到 x ,此时 meta_atrribute == null 时),则抛出 AttributeError 异常,结束查找。若找到的 meta_atrribute(meta_atrribute != null 时)是 non-data descriptor 则执行
meta_atrribute.__get__(b, type(b))
,并将执行结果返回,结束查找;否则直接返回 meta_atrribute, 结束查找。
cpython对应的底层实现代码: type_getattro()
in Objects/typeobject.c。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100/* This is similar to PyObject_GenericGetAttr(),
but uses _PyType_Lookup() instead of just looking in type->tp_dict. */
static PyObject *
type_getattro(PyTypeObject *type, PyObject *name)
{
PyTypeObject *metatype = Py_TYPE(type);
PyObject *meta_attribute, *attribute;
descrgetfunc meta_get;
if (!PyString_Check(name)) {
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
name->ob_type->tp_name);
return NULL;
}
/* Initialize this type (we'll assume the metatype is initialized) */
if (type->tp_dict == NULL) {
if (PyType_Ready(type) < 0)
return NULL;
}
/* No readable descriptor found yet */
meta_get = NULL;
/* Look for the attribute in the metatype */
meta_attribute = _PyType_Lookup(metatype, name);
/*如果在元类中找到对应名称的data descriptor属性,则执行 __get__()方法,
*将结果返回,结束查找
*/
if (meta_attribute != NULL) {
meta_get = Py_TYPE(meta_attribute)->tp_descr_get;
if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
/* Data descriptors implement tp_descr_set to intercept
* writes. Assume the attribute is not overridden in
* type's tp_dict (and bases): call the descriptor now.
*/
return meta_get(meta_attribute, (PyObject *)type,
(PyObject *)metatype);
}
Py_INCREF(meta_attribute);
}
/* No data descriptor found on metatype. Look in tp_dict of this
* type and its bases */
attribute = _PyType_Lookup(type, name);
if (attribute != NULL) {
/* Implement descriptor functionality, if any */
descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get;
Py_XDECREF(meta_attribute);
/*在类的__dict__中找到定义了 __get__() 方法的descriptor属性,则执行
* __get__()方法,将结果返回,结束查找。
*
* 这里与在object.__dict__中查找到属性后的处理方式不同:在object中直接
* 将属性属性返回;在type.__dict__中找到属性后如果属性是定义了 __get__()
* 方法的descriptor,要执行__get__(None, type)返回结果。
*/
if (local_get != NULL) {
/* NULL 2nd argument indicates the descriptor was
* found on the target object itself (or a base) */
return local_get(attribute, (PyObject *)NULL,
(PyObject *)type);
}
/*在类__dict__中找到的属性不是descriptor时,直接将属性返回,结束查找*/
Py_INCREF(attribute);
return attribute;
}
/*若前面没有在元类中找到属性,直接抛出异常,结束查找;
*若在元类中找到的属性不是descriptor,直接返回,结束查找。
*若在元类中找到的属性是non-data descriptor, 则执行 __get__()方法,
*将结果返回,结束查找。
*/
/* No attribute found in local __dict__ (or bases): use the
* descriptor from the metatype, if any */
if (meta_get != NULL) {
PyObject *res;
res = meta_get(meta_attribute, (PyObject *)type,
(PyObject *)metatype);
Py_DECREF(meta_attribute);
return res;
}
/* If an ordinary attribute was found on the metatype, return it now */
if (meta_attribute != NULL) {
return meta_attribute;
}
/* Give up */
PyErr_Format(PyExc_AttributeError,
"type object '%.50s' has no attribute '%.400s'",
type->tp_name, PyString_AS_STRING(name));
return NULL;
}
3.3 在 object.__getattribute__()
与 type.__getattribute__()
对descriptor不同处理
由上面的介绍可见,object.__getattribute__()
与 type.__getattribute__()
在实例字典(object.__dict__, type.__dict__
) 找到的descriptor类型属性的处理方式不同(步骤3)。object中是直接返回,type中会用调用__get__(None, type),返回结果(注意调用的参数)。
之前在阅读python描述器的时候,读到object与type的__getattribute__()方法对描述器的处理方式区别,作者用pure python代码描述了用object.__getattribute__()等价实现的type.__getaatribute__()一直不能理解。现在结合前述解析,应该很好理解了,注意代码中self实际是一个class:
1 | def __getattribute__(self, key): |
3.4 super 属性查找过程
super()
返回的 super object 包含一个特别定制的 __getattribute__()
方法,该方法对属性的查找过程与 object 和 type 的略有不同。 super()
返回的对象在 cpython 中对应 c 类型是 PySuper_Type
,其属性查找的方法是 super_getattro()
(详见代码 Objects/typeobject.c
,也就是说 super
本身是一个特殊的内建类,与 type,str,int 一样)。
super
根据初始化参数的不同可以有 3 中使用方式:
super(type, obj),返回一个已经绑定的
super object
,参数要满足isinstance(obj, type)
;super(type, type2),返回一个已经绑定的
super object
,参数要满足issubclass(type2, type)
;super(type),返回一个未绑定的
super object
。
super object
的 __getattribute__()
查找过程,简单来说就是:
对于 “已绑定” 的
super object
, 首先要沿着start_type.__mro__
查找属性,如果没有找到则委托object.__getattribute__()
在 “super object” 中查找。对于 “未绑定” 的
super object
则直接委托object.__getattribute__()
在 “super object” 中查找。
前面第 1 步中提到的 start_type
在通过 super(type, obj)
使用时指的是 type(obj)
或者 obj.__class__
,通过 super(type, type2) 使用时指的是 type2
。 super(type, obj)
时,正常模式下 issubclass(type(obj), type)
则使用 type(obj)
,否则若 issubclass(obj.__class__, type)
则使用 obj.__class__
。出现 type(obj)
与 obj.__class__
是为了兼容代理类模式,若 obj
是代理类型的实例,那么 type(obj)
得到的是 “proxy class” (代理类型),这个时候 obj.__class__
才是真正代理对象的类型。这里有一个透明代理的例子:Object Proxying (Python recipe)
(++注:实际上你可以这么理解: super 构造器的第 1 参数决定搜索的起始位置,第 2 个参数决定搜索的范围也就是搜索哪个类型的 mro,没有第 2 个参数便没有 mro,也就谈不上第 1 个。++)
综上所述,对于未绑定的 super object
其查找属性的方式和 object.__getattribute__()
是相同的。对于已经绑定的则不同,基于前面定义的 start_type
,下面我们来说一下详细的查找过程:
从
start_type.__mro__
中查找到与 B 紧邻的基类 A,然后从 A 开始沿着start_type.__mro__
搜索链中每个基类的__dict__
,若m
存在(即__dict__['m']
存在):若m
是描述器,则返回__get__(obj, obj.__class__)
,否则直接返回m
;若m
不存在,则进入下一步。委托
object.__getattribute__()
在 “super object” 中进行查找。
下面结合 Objects/typeobject.c
中对应的代码来看看实现过程:
1 | static int |
前面的代码负责构建一个 super object
,并确定好该对象的:type,obj,obj_type
字段。下面来看看 super_getattro
函数:
1 | static PyObject * |
3.5 属性赋值时的查找过程
python中对于object和type的属性赋值查找过程是相同的。Objects/typeobject.c 的type_setattro()方法最终调用也是 Objects/object.c 的 PyObject_GenericSetAttr()方法。如下c的 type_setattro() 代码所示:
1 | static int |
Objects/object.c 的 PyObject_GenericSetAttr()中对属性赋值的过程是:
在type(obj) [注:对于object是class,对于class是metaclass] 的继承链中查找属性,若找到的属性是data descriptor,则调用__set__(obj, value)方法设置属性,操作完成返回。否则进去下一步。
如果obj.__dict__存在,直接在obj.__dict__中加入属性,操作完成返回。否则进去下一步。
若在第1步type(obj)的继承链中找到的属性是descriptor,且定义了__set__方法,则调用__set__(obj, value)方法设置属性,操作完成返回。
抛出属性只读异常(比如用@property装饰的属性)。
1 | int |
3.5 object.__getattr__(self, name)
与 object.__getattribute__(self, name)
通过自定义 object.__getattr__ 和 object.__getattribute__ 方法,我们可以控制对象属性访问过程。一般而言,访问对象属性时 ++首先调用 object.__getattribute__ 方法,当该方法抛出 AttributeError 异常时,调用 object.__getattr__ 方法,并返回调用结果。++
object.__getattribute__(self, name)
在访问类实例的属性时无条件调用这个方法。 如果类也定义了方法 __getattr__() ,那么除非 __getattribute__() 显式地调用了它,或者抛出了 AttributeError 异常, 否则它就不会被调用。 这个方法应该返回一个计算好的属性值, 或者抛出异常 AttributeError。为了避免无穷递归,对于任何它需要访问的属性, 这个方法应该调用基类的同名方法,例如, object.__getattribute__(self, name).注意: 通过特定语法或者内建函式, 做隐式调用搜索特殊方法时,这个方法可能会被跳过,参见 搜索特殊方法。
object.__getattr__(self, name)
在正常方式访问属性无法成功时 (就是说, self 属性既不是实例的, 在类树结构中找不到) 使用。 name 是属性名,该方法应该返回一个计算好的属性值或抛出一个 AttributeError 异常。注意, 如果属性可以通过正常方法访问, __getattr__() 是不会被调用的 (是有意将 __getattr__() 和 __setattr__() 设计成不对称的)。这样做的原因是基于效率的考虑,并且这样也不会让 __getattr__() 干涉正常属性。 至少对于类实例而言, 不必非要更新实例字典来操作属性 (可以将它们保存到实例的其它字段中)。需要全面控制属性访问,可以参考上面关于 __getattribute__() 的介绍。
NOTE:若按照默认方式执行属性赋值,最后也是保存在实例的 __dict__ 中,通过重写 __setattr__() 方法可直接将属性放到 __dict__ 中。 不必非得按标准方式搜索一圈回来再保存属性到 __dict__ ,效率更高。这就是不对称设计考量的效率问题。
对于对象属性赋值的控制,python提供了object.__setattr__方法:
object.__setattr__(self, name, value)
在属性要被赋值时调用。这会替代正常机制 (即把值保存在实例字典中)。name 是属性名, vaule 是要赋的值.
如果在 __setattr__() 里要对一个实例属性赋值, 它应该调用父类的同名方法,例如, object.__setattr__(self, name, value)。
3.6 __dict__ 与 __slots__
python 在实例化一个类时,默认会为每个实例创建一个字典用来存储实例属性。该字典通过实例属性 __dict__
暴露出来的, 其背后实现的技术也是 descriptor 描述器: 在类的 __dict__
中定义了一个名叫 ” __dict__ “ 的 data descriptor。结合之前的属性访问机制,我们很容易理解下面的代码:
1 | class A(object):pass |
也就是说, 通过 obj.__dict__
首先访问到类的资料描述器,描述器执行 desc.__get__()
后才真正返回实例内部的字典。
对于那些没有什么实例属性且会创建大量实例的类型而言,这种默认创建实例字典的方式会造成不必要的内存浪费。 考虑到这个问题,python 在新式类中引入了 __slots__
属性。当一个类定义了 __slots__
属性,在实例化该类时便不会为实例分配用于保存实例属性的字典(注:所以通过默认方式为实例增加属性是不可能的,没地方存储。同时该类也不会生成前面所说的名为 “__dict__“ 的描述器,所以尝试访问 obj.__dict__
来保存也不行),而是预先分配为 __slots__
声明的属性分配好空间,并在类型的 __dict__
中为对应属性保存一个资料描述器。
1 | class A(object):__slots__ = ("s",) |
有一个需要注意的问题,在定义了 __slots__
的类型中,若没有在其实例上定义相应的属性(为属性赋值),那么其背后的 member_descriptor
调用会抛出 AttributeError 异常。换句话说,定义了 __slots__
的类型实例化时并不会为属性提供默认值。
1 | # 没有在实例上为属性赋值,访问会出错 a.s |
4 结束语
通过对python属性查找过程的解析,我们初步了解了其背后实现的机制,对于深入理解这门语言是大有裨益的。关于metaclass、MRO、Descriptor更深入的内容,可参考后面的资料。
我使用python也有一段时间了,但是对语言背后的实现细节一直没有系统地研究过。每一次涉及到相关的问题,都是浅尝辄止,总是抱着问题解决就结束的态度。也就导致一次一次重复去查阅相关资料,浪费了时间,也没有真正理解。对其中很多底层技术和概念都有所了解,但没有深入,也没有思考过其中的联系。借由这次遇到的问题,整理了一下,这篇文字算是一个总结。
5 参考资料
Guido van Rossum 撰写的 Method Resolution Order 详细介绍了python的MRO算法演变历史。
Michele Simionato 撰写的 The Python 2.3 Method Resolution Order 详细介绍了python2.3 MRO采用的C3算法。
e-satis 在Stack Overflow回答提问 What is a metaclass in Python? 的经典回答,通俗易懂地讲解了python的metaclass。
python文档中详细介绍了 __slots__。