• 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列表中

    我们跟踪这个遍历的过程来看一下:

    1. mro列表(tp_mro)中没有D,所以先获得D
    2. 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的处理推迟
    3. 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
    ", *(type->tp_as_number->nb_add));
        }
        //打印int中的nb_add地址
        if (strcmp(type->tp_name, "int") == 0) {
            printf("int nb_add: 0x%X
    ", *(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列表从基类继承操作
    • 设置基类的子类列表
  • 相关阅读:
    第04组 团队项目-需求分析报告
    团队项目-选题报告
    第二次结对编程作业
    第一次结对编程作业
    第四组 团队展示
    第一次博客作业
    第09组 Alpha事后诸葛亮
    第09组 Alpha冲刺(6/6)
    2019 SDN上机第4次作业
    第09组 Alpha冲刺(5/6)
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9621918.html
Copyright © 2020-2023  润新知