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正是操作排序的关键所在

 

posted @ 2018-09-10 19:04  北洛  阅读(1478)  评论(0编辑  收藏  举报