Python虚拟机类机制之descriptor(三)

时间:2022-01-12 04:29:14

从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

Python虚拟机类机制之descriptor(三)

图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所示:

Python虚拟机类机制之descriptor(三)

图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所示:

Python虚拟机类机制之descriptor(三)

图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'>

  

Python虚拟机类机制之descriptor(三)

图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列表从基类继承操作
  • 设置基类的子类列表