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列表从父类继承操作
- 设置父类的子类列表