New Style Class
1. Python中的对象模型
Python2.2之前内置的type(例如int, dict)与定义的class并不完全相同. Python在2.2版本之后弥补了这个鸿沟, 使得两者能在概念上实现了一致, 该机制称作new style class机制.
Python2.2前有三种对象:
- type: Python内置类型
- class: 程序员创建的类型
- instance: 由class创建的实例
Python2.2后只有两种对象:
- class对象: 内置类型和程序员创建的类型
- instance对象: 由class创建的实例
1.1 对象间的关系
Python的对象间存在两种关系:
- is-kind-of: 父类与子类之间的关系
- is-instance-of: 类与实例之间的关系
例如:
class A(object): |
这其中包含三个对象:
object(class对象)A(class对象)a(instance对象)
其中, object和A之间为is-kind-of关系, A和object之间为is-instance-of关系.
Python提供了两种内置方法issubclass和isinstanceof来判断两个对象间是否存在is-kind-of和is-instance-of关系.
a.__class__ |
可以看到, 并不是所有对象都有is-kind-of关系, 只有class对象之间才存在is-kind-of关系. 下图更能说明三者的关系:
1.2 <type 'type'>和<type 'object'>
上图之所以将object和A放在一起, 因为他们都属于<type 'type'>. <type 'type'>是一种特殊的class, 其可作为其他class对象的type, 因此也称为metaclass.
Python中还有一个特殊的class: <type 'object'>, 任何class都必须直接或间接继承该class. 下面是<type 'type'>和<type 'object'>的关系:
object.__class__ |

中间的一列既可作为class又可作为instance, 因为它既可创建新的instance, 也可作为一个instance.
2. 从type到class
class MyInt(int): |
上述代码从int类型继承并生成一个新的整数类型, 做加法时会在原有的和上再加10. 调用int.__add__时, 需要PyInt_Type完成加法操作. 但Python虚拟机如何从int.__add__知道要调用PyInt_Type.tp_as_number.nb_add呢? 由于Python2.2之前的内置类型没有寻找某个属性的机制, 所以不能继承内置类型.
调用int.__add__时, 会通过PyInt_Type中tp_dict所指向的dict对象中查找__add__所对应的函数, 并调用该函数. 如下图:
Python中有一个非常重要的概念: 可调用性. 只要对象实现了__call__, 则该对象就会成为一个可调用对象. "调用"就是执行tp_call操作:
class A(object): |
C++中可通过重载来实现Functor. Python则通过调用PyObject_Call函数来对a进行操作, 从而调用__call__, 实现可调用性. 若对象不可调用, 则抛出异常:
def f(): |
上述代码编译成功, 但是运行时抛出异常, 说明可调用性不是编译期间决定的, 而是运行时决定的. 从Python2.2开始, 每次启动Python都会对对象系统进行初始化, 并动态地向PyTypeObject中填充一些东西(如果忘了什么是PyTypeObject, 可以参考Python的PyObject, PyObject中的_typeobject就是PyTypeObject, 其中也包括tp_dict), 从而完成从type对象到class对象的转变, 这一系列初始化的操作从_Py_ReadyTypes开始.
2.1 处理父类和type信息
int PyType_Ready(PyTypeObject *type) |
Python会首先尝试获得该类型的父类, 这个信息在PyTypeObject.tp_base中指定, 下列是内置class对象的tp_base信息:
| class对象 | 父类信息 |
|---|---|
| PyType_Type | NULL |
| PyInt_Type | NULL |
| PyBool_Type | &PyInt_Type |
如果tp_base内置了class对象, 则用指定的父类; 如果没有, 则使用默认父类: PyBaseObject_Type, 也就是<type 'object'>. 所以Python所有class对象都直接或间接以<type 'object'>作为父类, 由于PyType_Type也以<type 'object'>作为父类.
之后Python会判断父类是否初始化, 通过判断base->tp_dict是否为NULL.
最后设置对象的ob_type, 也就是metaclass. Python直接将父类的metaclass作为子类的metaclass. PyType_Type的metaclass为<type 'object'>的metaclass, 而PyBaseObject_Type的ob_type为PyType_Type, 所以PyType_Type的metaclass为其自身.
2.2 处理父类列表
由于Python支持多重继承, 所以每个class对象都有一个父类列表:
int PyType_Ready(PyTypeObject *type) |
对于PyBaseObject_Type, tp_base为NULL, base也为NULL, 所以父类列表为空tuple.
对于PyType_Type和其他类型, 例如PyInt_Type, 虽然tp_bases为NULL, 但base为&PyBaseObject_Type. 所以它们的父类不为NULL, 而是包含一个PyBaseObject_Type.
2.3 填充tp_dict
int PyType_Ready(PyTypeObject *type) |
该阶段将各种操作,属性等加入到PyTypeObject中. 但Python又是如何将__add__和nb_add关联起来的: Python其实将这种关联放在了slotdefs的全局数组中
2.3.1 slot与操作排序
Python中用slot来表示PyTypeObject中的操作, 一个操作对应一个slot. slot中不仅仅是函数指针, 还包括其他信息.
typedef struct wrapperbase slotdef; |
slot中name是操作对应的名称, 例如__add__; offset表示函数地址在PyHeapTypeObject中的偏移量; function指向名为slot的函数.
Python提供了多个宏来定义slot, 最基本的是TPSLOT和ETSLOT
TPSLOT中的offset是PyTypeObject的偏移量, ETSLOT计算的是PyHeapTypeObject的偏移量. 由于PyHeapTypeObject的第一个域为PyTypeObject, 所以TPSLOT计算的offset也是PyHeapTypeObject的offset.
对于nb_add来说, 函数指针放在PyNumberMethods中, 而PyTypeObject却通过tp_as_number指向另一个PyNumberMethods结构. 所以offset根本没法用于PyTypeObject中偏移量的计算, 只能计算PyHeapObject中的偏移量.
typedef struct _heaptypeobject { |
然后PyInt_Type是一个PyTypeObject, 而offset针对的是PyHeadTypeObject, 无论如何都不能从PyHeapTypeObject中偏移到PyTypeObject.
offset主要为操作进行排序, 先看slotdefs:
|
BINSLOT, SQSLOT都是对ETSLOT的一个简单包装, 而且操作名(例如__add__)和操作函数并不是一一对应, 因为多个操作可能对应着同一个操作名.
当某个类型调用某个操作时, 就需要决定调用哪个操作函数. 例如:
class A(list): |
上述代码中A的__getitem__对应PyList_Type中的mp_subscript和sq_item, 最后选择的是list_subscript. 其中某个操作函数被调用就涉及操作优先级的问题, 这时就需要offset来区分优先级. 这个例子中, offset(mp_subscript) < offset(sq_item)
整个slotdefs的排序在init_slotdefs中完成:
static void init_slotdefs(void) |
2.3.2 从slot到descriptor
tp_dict作为一个字典, 与__getitem__相关联的一定不是一个slot, 因为slot不是一个PyObject, 无法作为字典的键值. 并且slot由于不是一个PyObject, 所以也无法被调用.
所以就需要一个PyObject来包装slot, 这样才能和__getitem__关联起来, 并将这个PyObject称之为descriptor.
Python内部含有多个decriptor, 与PyTypeObject对应的是PyWrapperDescrObject. 一个descriptor包含一个slot, 下面是descriptor的创建函数PyDescr_NewWrapper:
|
PyDescr_COMMON作为每一种descriptor的基础部分, d_type作为参数type(PyWrapperDescrObject的type为PyWrapperDescr_Type), d_wrapped作为函数指针. 对于PyList_Type来说, tp_dict["__getitem__"].d_wrapped为&mp_subscript, slot存放在d_base中.
2.3.3 建立联系
函数排序的结果仍放在slotdefs中, 但Python会从头到尾扫描slotdefs并为每一个slotdef创建一个descriptor, 然后为每一个操作名创建与descriptor的关联, 整个创建过程如下:
static int add_operators(PyTypeObject *type) |
上述代码的功能的难点在于slotptr函数, 它通过slot中的offset将type中的函数指针找出来. 但问题在于: offset是相对于PyHeapTypeObject, PyHeapTypeObject中包含了PyNumberMethods结构体, 但PyTypeObject中只包含了PyNumberMethods*. 所以offset对于PyTypeObject不可用, 必须经过转换.
举个例子来说, 假如调用slotptr(&PyList_Type, offset(PyHeapTypeObject, mp_subscript)), 首先由于mp_subscript的offset大于as_mapping的offset, 所以应先找到as_mapping指针, 然后从as_mapping指针开始进行偏移, 偏移量delta为:
delta = offset(PyHeapTypeObject, mp_subscript) - offset(PyHeapTypeObject, as_mapping) |
slotptr的完整实现如下:
static void** slotptr(PyTypeObject *type, int ioffset) |
之所以先从PySequenceMethods开始判断, 是因为PySequenceMethods更靠后; 如果先从靠前的位置开始判断, 那么就会错过靠后的位置, 从而导致偏移量计算错误.
从tp_as_mapping延伸出去的list_as_mapping编译时就定义好了, 但tp_dict是运行时再创建. 通过add_operators为PyType_Type添加一些operators后, 还会通过add_methods, add_members和add_getsets添加tp_methods, tp_members和tp_getset函数集.
虽然和add_operators类似, 但添加的descriptor不是PyWrapperDescrObject, 而分别是PyMethodDescrObject, PyMemberDescrObject和PyGetSetDescrObject.
2.3.4 确定MRO
MRO(Method Resolve Order)是一个class对象的属性解析顺序, 由于Python支持多重继承, 所以必须设置如何顺序解析属性
class A(list): |
由于D的父类A和B中都实现了show, 那么调用d.show()时, 该调用谁的show(). Python在PyType_Ready中通过mro_internal函数完成了对一个类型的mro顺序的建立. Python通过创建一个tuple对象来依次存储一组class对象, class对象的顺序就是Python解析属性时的mro顺序. 最终这个tuple被保存在PyTypeObject.tp_mro中.
Python在内部创建一个list, 根据D的声明顺序放入D和D的父类:
list的最后一项包含D的所有直接父类. Python从左向右遍历该list, 当访问到list钟任一个父类时, 如果父类存在mro列表, 则会访问父类的mro列表. 以下是遍历list的整个过程:
- 获得D, D的mro列表(tp_mro)中没有D, 放入D
- 获得C, D的mro列表没有C, 所以放入C, 由于C也有mro列表, 所以开始访问C的mro列表
- 获得A, D的mro列表没有A, 放入A
- 获得A的list, 但由于后面B的mro列表也有list, 那么A的list将被推迟
- 获得object, 并将object的处理推迟
- 获得B, D的mro没有B, 所以放入B, 转而访问B的mro列表:
- 获得list, 将list放入D的mro列表
- 获得object, 将object放入D的mro列表
遍历结束后D的mro列表就完成了tuple的创建: (D, C, A, B, list, object)
print D.__mro__ |
通过改变D的父类列表, 可以确定mro列表的顺序:
2.3.5 继承父类操作
Python确定了mro后会遍历mro列表(tp_mro), 并将class对象没有而父类有的操作拷贝到class对象中, 从而完成对父类操作的继承动作. 继承操作在inherit_slots中:
int PyType_Ready(PyTypeObject *type) |
inherit_slots会进行很多拷贝操作, 这里以nb_add为例:
static void inherit_slots(PyTypeObject *type, PyTypeObject *base) |
2.3.6 填充父类中的子类列表
最后一步是设置子类列表, 在每一个PyTypeObject中有一个tp_subclasses. 通过调用add_subclass向tp_subclasses中填充子类对象:
int PyType_Ready(PyTypeObject *type) |
我们可以验证这个子类列表的存在:
int.__subclasses__() |
以下是PyType_Ready的工作流程:
- 设置类型信息, 父类和父类列表
- 填充
tp_dict - 确定mro列表
- 基于mro列表从父类继承操作
- 设置父类的子类列表