Python虚拟机类机制之填充tp_dict(二)
填充tp_dict
在Python虚拟机类机制之对象模型(一)这一章中,我们介绍了Python的内置类型type如果要完成到class对象的转变,有一个重要的步骤就是填充tp_dict对象,这是一个极其繁杂的过程
typeobject.c
int PyType_Ready(PyTypeObject *type) { PyObject *dict, *bases; PyTypeObject *base; Py_ssize_t i, n; …… //设定tp_dict dict = type->tp_dict; if (dict == NULL) { dict = PyDict_New(); if (dict == NULL) goto error; type->tp_dict = dict; } //将与type相关的descriptor加入到tp_dict中 if (add_operators(type) < 0) goto error; if (type->tp_methods != NULL) { if (add_methods(type, type->tp_methods) < 0) goto error; } if (type->tp_members != NULL) { if (add_members(type, type->tp_members) < 0) goto error; } if (type->tp_getset != NULL) { if (add_getset(type, type->tp_getset) < 0) goto error; } …… }
在这个阶段,将完成("__add__", &nb_add)在tp_dict的映射。这个阶段的add_operators、add_methods、add_members、add_getset都是这样完成填充tp_dict的动作。那么,一个问题浮现了,Python虚拟机是如何知道"__add__"和nb_add之间存在关联呢?这种关联是在Python源代码中预先约定好的,存放在一个名为slotdefs的全局数组
slot与操作排序
在进入填充tp_dict的复杂操作之前,我们先来介绍Python内部的一个概念:slot。在Python内部,slot可以视为表示PyTypeObject中定义的操作,一个操作对应一个slot,但是slot不仅仅包含一个函数指针,它还包含其他一些信息。在Python内部,slot是通过slotdef这个结构体来实现的
//typeobject.c typedef struct wrapperbase slotdef; //descrobject.h struct wrapperbase { char *name; int offset; void *function; wrapperfunc wrapper; char *doc; int flags; PyObject *name_strobj; };
在一个slot中,存储着与PyTypeObject中一种操作相对应的各种信息。name就是操作对应的名称,如字符串__add__,offset则是操作的函数地址在PyHeapTypeObject中的偏移量,而function则指向一种称为slot function的函数,这里新引进一个类型PyHeapTypeObject,PyHeapTypeObject可是个好东西,它在以后分析用户自定义类型还会在用到,这里我们简单看一下PyHeapTypeObject的定义,以及PyType_Type中关于PyHeapTypeObject的定义,后续还会讲解PyHeapTypeObject
//object.h typedef struct _heaptypeobject { PyTypeObject ht_type; PyNumberMethods as_number; PyMappingMethods as_mapping; PySequenceMethods as_sequence; PyBufferProcs as_buffer; PyObject *ht_name, *ht_slots; } PyHeapTypeObject; //typeobject.c PyTypeObject PyType_Type = { …… sizeof(PyHeapTypeObject), /* tp_basicsize */ …… };
Python中提供了多个宏来定义一个slot,其中最基本的是TPSLOT和ETSLOT:
//typeobject.c #define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} #define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} //structmember.h #define offsetof(type, member) ( (int) & ((type*)0) -> member )
TPSLOT和ETSLOT的区别在于TPSLOT计算的是操作对应的函数指针在PyTypeObject中的偏移量,而ETSLOT计算的是函数指针在PyHeadTypeObject中的偏移量。但是观察下面列出的PyHeadTypeObject的代码,可以发现,因为PyHeadTypeObject的第一个域就是PyTypeObject,所以TPSLOT计算出的偏移量实际上也就是相对于PyHeadTypeObject的偏移量
对于一个PyTypeObject来说,有的操作,比如nb_add,其函数指针在PyNumberMethods中存放,而PyTypeObject中却是通过一个tp_as_number指针指向另一个PyNumberMethods结构,所以,实际上根本没有办法算出nb_add在PyTypeObject中的偏移量,只能计算其在PyHeadTypeObject这种的偏移量
因此,与nb_add对应的slot必须是通过ETSLOT来定义的。如果说与nb_add对应的slot中的记录的offset是基于PyHeadTypeObject的,而PyInt_Type却是一个PyTypeObject,那么显然通过这个偏移量是不可能得到PyInt_Type中为int准备的nb_add,那么这个offset有什么用呢?
其实这个offset是用来排序的,为了理解为什么需要对操作进行排序,需要来看看Python预定义的slot集合——slotdefs
typeobject.c
…… #define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} #define SQSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ ETSLOT(NAME, as_sequence.SLOT, FUNCTION, WRAPPER, DOC) #define MPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ ETSLOT(NAME, as_mapping.SLOT, FUNCTION, WRAPPER, DOC) #define BINSLOT(NAME, SLOT, FUNCTION, DOC) \ ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_l, \ "x." NAME "(y) <==> x" DOC "y") #define RBINSLOT(NAME, SLOT, FUNCTION, DOC) \ ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_r, \ "x." NAME "(y) <==> y" DOC "x") …… static slotdef slotdefs[] = { …… //不同操作名对应相同操作 BINSLOT("__add__", nb_add, slot_nb_add, "+"), RBINSLOT("__radd__", nb_add, slot_nb_add, "+"), //相同操作名对应不同操作 SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item, "x.__getitem__(y) <==> x[y]"), MPSLOT("__getitem__", mp_subscript, slot_mp_subscript, wrap_binaryfunc, "x.__getitem__(y) <==> x[y]"), …… };
其中,SQSLOT、MPSLOT、BINSLOT、RBINSLOT都是对ETSLOT的一个简单的包装,在slotdefs中,可以发现,操作名(比如__add__)和操作并不是一一对应的,存在着多个操作对应同一操作名的情况,同样也存在着同一个操作对应不同操作名的情况。对于相同操作名对应不同操作的情况,在填充tp_dict时,就会出现问题,比如对于__getitem__,在填充tp_dict中与其对应的是sq_item还是mp_subscript呢?
为了解决这个问题,就需要利用slot中的offset信息对slot(也就是对操作)进行排序。回顾一下前面的PyHeadTypeObject的代码,它与一般的struct定义不同,其实定义中各个域的顺序也是相当关键的,在顺序中隐含着操作优先级的信息。比如在PyHeadTypeObject中,PyMappingMethods的位置在PySequenceMethods之前,mp_subscript是PyMappingMethods中的PyObject*,而sq_item是PySequenceMethods中的PyObject*,所以最终计算出的偏移存在如下的关系:offset(mp_subscript)<offset(sq_item)。
如果一个PyTypeObject中,既定义了mp_subscript又定义了sq_item,那么Python虚拟机将选择mp_subscript与__getitem__建立关系,而PyList_Type正是这样的一个PyTypeObject,在PyList_Type中,tp_as_mapping.mp_subscript指向list_subscript,而tp_as_sequence.sq_item指向list_item
listobject.c
PyTypeObject PyList_Type = { …… &list_as_sequence, /* tp_as_sequence */ &list_as_mapping, /* tp_as_mapping */ …… }; static PySequenceMethods list_as_sequence = { …… (ssizeargfunc)list_item, /* sq_item */ …… }; static PyMappingMethods list_as_mapping = { (lenfunc)list_length, (binaryfunc)list_subscript, (objobjargproc)list_ass_subscript };
现在,让我们在list_item和list_subscript两个方法中添加打印语句,看看究竟是执行list_item还是执行list_subscript
listobject.c
static PyObject * list_subscript(PyListObject* self, PyObject* item) { printf("call list_subscript\n"); if (PyIndex_Check(item)) { …… } …… } static PyObject * list_item(PyListObject *a, Py_ssize_t i) { printf("call list_item\n"); if (i < 0 || i >= a->ob_size) { …… } …… }
因为Python对list的索引元素的操作有优化,所以我们必须用一个类继承自list才能看到效果,A中的__getitem__对应的操作就是对PyList_Type中的mp_subscript和sq_item选择的结果,可以看到,list_subscript被选中了,但后面还跟着一个list_item。为什么会出现这样的情况?是因为在list_subscript函数中还调用了list_item
>>> a = A() >>> a.append(1) >>> print(a[0]) call list_subscript call list_item 1
slotdefs的排序在init_slotdefs中完成:
typeobject.c
static void init_slotdefs(void) { slotdef *p; static int initialized = 0; //init_slotdefs只会进行一次 if (initialized) return; for (p = slotdefs; p->name; p++) { //填充slotdef结构体中name_strobj p->name_strobj = PyString_InternFromString(p->name); if (!p->name_strobj) Py_FatalError("Out of memory interning slotdef names"); } //对slotdefs中的slotdef进行排序 qsort((void *)slotdefs, (size_t)(p-slotdefs), sizeof(slotdef), slotdef_cmp); initialized = 1; } //slot排序的比较策略 static int slotdef_cmp(const void *aa, const void *bb) { const slotdef *a = (const slotdef *)aa, *b = (const slotdef *)bb; int c = a->offset - b->offset; if (c != 0) return c; else return (a > b) ? 1 : (a < b) ? -1 : 0; }
在slot的排序策略函数slotdef_cmp中,可以清晰地看到,slot中的offset正是操作排序的关键所在