从slot到descriptor
在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__getitem__关联在一起的,一定不会是一个slot,原因很简单,slot不是一个PyObject,它不能存放在dict对象中。当然,我们再深入思考一下,会发现slot也不会被“调用”。既然slot不是一个PyObject,那么它就没有type,也就无从谈起什么tp_call了,所以slot是无论如何也不满足前面所描述的Python的“可调用”这个概念
前面我们说过,Python虚拟机会在tp_dict找到__geiitem__对应的操作后,调用该操作,所以在tp_dict中与__getitem__对应的只能是另一个包装了slot的PyObject,在Python中,我们称为descriptor
在Python内部,存在多种descriptor,与PyTypeObject中的操作对应的是PyWrapperDescrObject。在此后的描述,我们将用术语descriptor来专门表示PyWrapperDescrObject。一个descriptor包含一个slot,其创建是通过PyDescr_NewWrapper
descrobject.h
#define PyDescr_COMMON \
PyObject_HEAD \
PyTypeObject *d_type; \
PyObject *d_name
typedef struct {
PyDescr_COMMON;
struct wrapperbase *d_base;
void *d_wrapped; /* This can be any function pointer */
} PyWrapperDescrObject;
descrobject.c
static PyDescrObject *
descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
{
PyDescrObject *descr;
//申请空间
descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
if (descr != NULL) {
Py_XINCREF(type);
descr->d_type = type;
descr->d_name = PyString_InternFromString(name);
if (descr->d_name == NULL) {
Py_DECREF(descr);
descr = NULL;
}
}
return descr;
}
PyObject *
PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped)
{
PyWrapperDescrObject *descr;
descr = (PyWrapperDescrObject *)descr_new(&PyWrapperDescr_Type,
type, base->name);
if (descr != NULL) {
descr->d_base = base;
descr->d_wrapped = wrapped;
}
return (PyObject *)descr;
}
Python内部的各种descriptor都将包含PyDescr_COMMON,其中的d_type被设置为PyDescr_NewWrapper的参数type,而d_wrapped则存放着最重要的信息:操作对应的函数指针,比如对于PyList_Type来说,其tp_dict["__getitem__"].d_wrapped就是&mp_subscript。而slot则被存放在了d_base中
PyWrapperDescrObject的type是PyWrapperDescr_Type,其中的tp_call是wrapperdescr_call,当Python虚拟机调用一个descriptor时,也就会调用wrapperdescr_call,对于descriptor的调用过程,后面还会详细解析
建立联系
排序后的结果仍然存放在slotdefs中,Python虚拟机就可以从头到尾遍历slotdefs,基于每一个slot建立一个descriptor,然后在tp_dict中建立从操作名到descriptor的关联,这个过程在add_operators中完成
typeobject.c
static int add_operators(PyTypeObject *type)
{
PyObject *dict = type->tp_dict;
slotdef *p;
PyObject *descr;
void **ptr;
//对slotdefs进行排序
init_slotdefs();
for (p = slotdefs; p->name; p++) {
//如果slot中没有指定wrapper,则不处理
if (p->wrapper == NULL)
continue;
//获得slot对应的操作在PyTypeObject中的函数指针
ptr = slotptr(type, p->offset);
if (!ptr || !*ptr)
continue;
//如果tp_dict中存在操作名,则放弃
if (PyDict_GetItem(dict, p->name_strobj))
continue;
//创建descriptor
descr = PyDescr_NewWrapper(type, p, *ptr);
if (descr == NULL)
return -1;
//将(操作名,descriptor)放入tp_dict中
if (PyDict_SetItem(dict, p->name_strobj, descr) < 0)
return -1;
Py_DECREF(descr);
}
if (type->tp_new != NULL) {
if (add_tp_new_wrapper(type) < 0)
return -1;
}
return 0;
}
在add_operators中,首先会调用前面剖析过的init_slotdefs函数进行排序,然后遍历排序完后的slotdefs结构体数组,对其中每一个slot(slotdef),通过slotptr获得该slot对应的操作在PyTypeObject中的函数指针,并接着创建descriptor,在tp_dict中建立从操作名(slotdef.name_strobj)到操作(descriptor)的关联
需要注意的是,在创建descriptor之前,Python虚拟机会检查在tp_dict中操作名是否已存在,如果已经存在,则不会再次建立从操作名到操作的关联。正是这种检查机制与上面的排序机制相结合,使得Python虚拟机能够在拥有相同操作名的多个操作中选择优先级最高的操作
在add_operators中,上面描述的动作都很直观、简单。而最难的动作隐藏在slotptr这个函数中,它的功能是完成slot到slot对应操作的真实函数指针的转换。我们已经知道,在slot中存放着操作的offset,但很不幸,这个offset是相对于PyHeadTypeObject的偏移,而操作的真实函数指针则在PyTypeObject中指定。更不幸的是,PyTypeObject和PyHeadTypeObject不是同构的,因为PyHeadTypeObject中包含了PyNumberMethods结构体,而PyTypeObject中只包含了PyNumberMethods*指针。所以slot中存储的这个关于操作的offset对于PyTypeObject来说,不可能直接使用,必须通过转换
举个例子,假如说调用slotptr(&PyList_Type, offset(PyHeadTypeObject, mp_subscript)),首先判断这个偏移大于offset(PyHeadTypeObject, as_mapping),所以会先从PyTypeObject对象中获得as_mapping指针P,然后在P的基础上进行偏移就可以得到实际的函数地址了,而偏移量delta为:
delta = offset(PyHeadTypeObject, mp_subscript) - offset(PyHeadTypeObject, as_mapping)
这个复杂的转换过程在slotptr中完成:
typeobject.c
static void ** slotptr(PyTypeObject *type, int ioffset)
{
char *ptr;
long offset = ioffset;
assert(offset >= 0);
assert((size_t)offset < offsetof(PyHeapTypeObject, as_buffer));
//判断从PyHeapTypeObject中排后面的PySequenceMethods开始
if ((size_t)offset >= offsetof(PyHeapTypeObject, as_sequence)) {
ptr = (char *)type->tp_as_sequence;
offset -= offsetof(PyHeapTypeObject, as_sequence);
}
else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_mapping)) {
ptr = (char *)type->tp_as_mapping;
offset -= offsetof(PyHeapTypeObject, as_mapping);
}
else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_number)) {
ptr = (char *)type->tp_as_number;
offset -= offsetof(PyHeapTypeObject, as_number);
}
else {
ptr = (char *)type;
}
if (ptr != NULL)
ptr += offset;
return (void **)ptr;
}
为什么判断首先从PySequenceMethods开始,然后向前,依次判断PyMappingMethods和PyNumberMethods呢?假如我们先从PyNumberMethods开始判断,如果一个操作的offset大于PyHeadTypeObject中as_number在PyNumberMethods的偏移量,那么我们还是没有办法确定在这个操作是属于PyNumberMethods还是属于PyMappingMethods或PySequenceMethods。只有从后往前进行判断,才能解决这个问题
现在,我们摸清楚Python在改造PyTypeObject时对tp_dict做了什么,图1-1显示了PyList_Type完成初始化之后的整个布局,其中包括我们讨论的descriptor和slot
图1-1 add_operators完成之后的PyList_Type
在图1-1中,PyList_Type.tp_as_mapping中延伸出去的部分是在编译时已经确定好的,而从tp_dict中延伸出去的部分是在Python运行时环境初始化才建立的。
PyType_Ready在通过add_operators添加了PyTypeObject对象中定义了的一些操作后,还会通过add_methods、add_members、add_getset添加在PyTypeObject中定义的tp_methods、tp_members和tp_getset函数集,这些过程与add_operators类似,不过最后添加到tp_dict中的descriptor就不再是PyWrapperDescrObject,而分别是PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject
图1-1所显示的class对象大部分正确,但还不算全部正确,考虑下面的例子:
>>> class A(list):
... def __repr__(self):
... return "Python"
...
>>> s = "%s" % A()
>>> s
'Python'
熟悉Python的人都知道,__repr__是Python中的特殊方法。当Python执行表达式"s = '%s' %A()"时,最终会调用A.tp_repr。如果按照图1-1的布局,并且对照PyList_Type,那么就应该调用list_repr这个函数,但并不是这样的,Python虚拟机最终调用的是A中重写后的__repr__。这意味着,Python在初始化A时,对tp_repr进行了特殊处理。为什么Python虚拟机会知道要对tp_repr进行特殊处理呢?答案还是在slot身上
在slotdefs中,有一条slot为TPSLOT:
typeobject.c
TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc, "x.__repr__() <==> repr(x)")
Python虚拟机在初始化A时,会检查<class A>的tp_dict中是否存在__repr__。在后面剖析用户自定义的class对象时,我们会看到,因为在定义class A时重写__repr__这个操作,所以A.tp_dict中__repr__一开始就会存在,Python虚拟机会检测到它的存在。一旦检测到__repr__存在,Python虚拟机将tp_repr这个函数指针替换为slot中指定的&slot_tp_repr。所以当Python虚拟机调用A.tp_repr时,实际上执行的是slot_tp_repr
typeobject.c
static PyObject * slot_tp_repr(PyObject *self)
{
PyObject *func, *res;
static PyObject *repr_str;
//[1]:查找__repr__属性
func = lookup_method(self, "__repr__", &repr_str);
if (func != NULL) {
//[2]:调用__repr__对应的对象
res = PyEval_CallObject(func, NULL);
Py_DECREF(func);
return res;
}
PyErr_Clear();
return PyString_FromFormat("<%s object at %p>",
self->ob_type->tp_name, self);
}
在slot_tp_repr中,会寻找__repr__属性对应的对象,正好就会找到我们在A中重写的函数,这个对象其实是一个PyFunctionObject。这样一来,就完成了对默认list的repr行为的替换,所以对A来说,其初始化结束后的内存布局则如图1-2所示:
图1-2 初始化完成后的A
当然,并是不会A中所有的操作都会有这样的变化。A的其他操作还是会指向PyList_Type中指定的函数,比如tp_iter还是会指向list_iter。对于A来说,这个变化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,对于内置class对象,不会进行这样的操作,这个操作是属于创建自定义class对象时的动作
对于A来说,这个变化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,对于内置class对象,不会进行这样的操作,这个操作是属于创建自定义class对象时的动作
确定MRO
所谓的MRO,即是指Method Resolve Order,更一般地,也是一个class对象的属性解析顺序。如果Python像java那样仅支持单继承,那就不是一个问题了。但是Python是支持多继承的,在多重继承时,就必须设置按照何种顺序解析属性,考虑如下Python代码:
>>> class A(list):
... def show(self):
... print("A::show")
...
>>> class B(list):
... def show(self):
... print("B::show")
...
>>> class C(A):
... pass
...
>>> class D(C, B):
... pass
...
>>> d = D()
>>> d.show()
A::show
由于D的基类A和B中都实现了show,那么在调用d.show()时,究竟是调用A的show方法还是B的show方法呢?Python内部在PyType_Ready中通过mro_internal函数完成了对一个类型的mro顺序的建立。Python虚拟机将创建一个tupple对象,在对象中依次存放着一组class对象。在tupple中,class对象的顺序就是Python虚拟机在解析属性时的mro顺序。最终这个tupple将被保存在PyTypeObject.tp_mro中
对于上述的class D,Python虚拟机会在内部创建一个list,其中根据D的声明依次放入D和它的基类,如图1-3所示:
图1-3 D建立mro列表时Python虚拟机内部的辅助list
注意在list的最后一项存放着一个包含所有D的直接基类列表。Python虚拟机将从左到右遍历该list,当访问到list中的任一个基类时,如果基类存在mro列表,则会转而访问基类的mro列表。在访问的过程中,不断将所访问到的class对象放入到D自身的mro列表中
我们跟踪这个遍历的过程来看一下:
- mro列表(tp_mro)中没有D,所以先获得D
- D的mro列表没有C,所以放入C,现在Python虚拟机发现C中存在mro列表,所以转而访问C的mro列表。:(1)D的mro列表中没有A,放入A;(2)接下来是list,这里需要注意,尽管D的mro列表没有list,但是后面的B的mro列表中出现了list,那么Python虚拟机会跳过这里的list,将list的获得推迟到处理B的mro列表时;(3)list之后是object,同样,将对object的处理推迟
- D的mro列表中没有B,所以放入B,转而访问B的mro列表:(1)处理list,这时可以将list放入D的mro列表;(2)处理object,这时可以将object放入D的mro列表
当遍历的过程结束后,D的mro列表也就存储了一个class对象的顺序列表了。从上面的遍历过程可以看到,这个列表是(D、C、A、B、list、object),我们可以来验证一下:
>>> for t in D.__mro__:
... print(t)
...
<class '__main__.D'>
<class '__main__.C'>
<class '__main__.A'>
<class '__main__.B'>
<class 'list'>
<class 'object'>
图1-4 展示不同顺序下mro列表
继承基类操作
Python虚拟机确定了mro列表后,就会遍历mro列表(注意,由于第一个class对象的mro列表的第一项总是其自身,所以遍历是从第二项开始的)。在mro列表中实际上还存储的就是class对象的所有直接和间接基类,Python虚拟机会将class对象自身没有设置而基类中设置了的操作拷贝到class对象中,从而完成对基类操作的继承动作:
这个继承操作的动作发生在inherit_slots中
typeobject.c
int PyType_Ready(PyTypeObject *type)
{
……
bases = type->tp_mro;
n = PyTuple_GET_SIZE(bases);
for (i = 1; i < n; i++) {
PyObject *b = PyTuple_GET_ITEM(bases, i);
if (PyType_Check(b))
inherit_slots(type, (PyTypeObject *)b);
}
……
}
在inherit_slots中,会拷贝相当多的操作,这里我们拿nb_add来做个例子:
typeobject.c
static void inherit_slots(PyTypeObject *type, PyTypeObject *base)
{
PyTypeObject *basebase;
#define SLOTDEFINED(SLOT) \
(base->SLOT != 0 && \
(basebase == NULL || base->SLOT != basebase->SLOT))
#define COPYSLOT(SLOT) \
if (!type->SLOT && SLOTDEFINED(SLOT)) type->SLOT = base->SLOT
#define COPYNUM(SLOT) COPYSLOT(tp_as_number->SLOT)
if (type->tp_as_number != NULL && base->tp_as_number != NULL) {
basebase = base->tp_base;
if (basebase->tp_as_number == NULL)
basebase = NULL;
COPYNUM(nb_add);
……
}
……
}
我们知道PyBool_Type中并没有设置nb_add操作,但它的tp_base设置的是&PyInt_Type,而PyInt_Type中却设置了nb_add操作。所以我们可以在PyType_Ready中添加输出语句,当处理type分别为bool和int时,输出其nb_add的地址,进行验证。因为按照inherit_slots的结果,这两个地址应该都指向同一个地址,即int_add的地址
typeobject.c
int PyType_Ready(PyTypeObject *type)
{
……
for (i = 1; i < n; i++) {
PyObject *b = PyTuple_GET_ITEM(bases, i);
if (PyType_Check(b))
inherit_slots(type, (PyTypeObject *)b);
}
//打印bool中的nb_add地址
if (strcmp(type->tp_name, "bool") == 0) {
printf("bool nb_add: 0x%X\n", *(type->tp_as_number->nb_add));
}
//打印int中的nb_add地址
if (strcmp(type->tp_name, "int") == 0) {
printf("int nb_add: 0x%X\n", *(type->tp_as_number->nb_add));
}
……
}
然后打开Python命令行,可以看到int类型和bool类型在初始化时打印其nb_add地址:
# ./python
int nb_add: 0x43C570
bool nb_add: 0x43C570
这个结果预示着Python中的两个bool对象,我们可以进行加法操作
填充基类中的子类列表
到这里,PyType_Ready还剩下最后一个重要的动作了:设置基类中的子类列表。在每一个PyTypeObject中,有一个tp_subclasses,这个东西在PyType_Type完成后将是一个list对象。其中存放着所有直接继承该类型的class对象。PyType_Ready通过调用add_subclass完成这个向tp_subclass中填充子类对象的动作
typeobject.c
int PyType_Ready(PyTypeObject *type)
{
PyObject *dict, *bases;
PyTypeObject *base;
Py_ssize_t i, n;
……
bases = type->tp_bases;
……
n = PyTuple_GET_SIZE(bases);
for (i = 0; i < n; i++) {
PyObject *b = PyTuple_GET_ITEM(bases, i);
if (PyType_Check(b) &&
add_subclass((PyTypeObject *)b, type) < 0)
goto error;
}
……
}
我们验证这个子类列表的存在:
>>> int.__subclasses__()
[<class 'bool'>]
>>> object.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>,……
>>>
果然,object是万物之母,很多的类都直接继承于object。可以看到,Python虚拟机对Python的内置类型对应的PyTypeObject进行了多种复杂的改造工作,总结一下,主要包括:
- 设置type信息,基类及基类列表
- 填充tp_dict
- 确定mro列表
- 基于mro列表从基类继承操作
- 设置基类的子类列表