Python虚拟机类机制之descriptor(三)
从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列表从基类继承操作
- 设置基类的子类列表