• 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
    ");
    	if (PyIndex_Check(item)) {
    		……
    	}
    	……
    }
    
    static PyObject * list_item(PyListObject *a, Py_ssize_t i)
    {	
    	printf("call list_item
    ");
    	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正是操作排序的关键所在

  • 相关阅读:
    jxl 单元格画斜线
    阿里云 tomcat 配置 注意
    java 的 数据库连接测试类 (SQL server)
    关于使用jackson.jar解析JSON时,大写JSON key值发生报错的问题
    JS配置文件动态加载CSS,js和定义请求路径
    SQL SERVER 2008升级SQL SERVER 2008 R2或者10.00.1600升级10.50.1600
    IDEA 启动运行的tomcat服务器项目,只能使用localhost访问的解决方法:
    舞会
    01序列
    交错01串
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9617229.html
Copyright © 2020-2023  润新知