楔子

在上一篇文章中我们说到,内置类对象虽然在底层静态定义好了,但还不够完善。解释器在启动之后还要再打磨一下,然后才能得到我们平时使用的类型对象,而这个过程被称为类型对象的初始化。

类型对象的初始化,是通过 Objects/typeobject.c 中的 PyType_Ready 函数实现的,它主要完成以下三个工作:

  • 给类型对象设置类型和基类信息;
  • 类对象属性字典的填充;
  • 类对象 MRO 的设置与属性继承;

我们来逐一解释。

给类型对象设置类型和基类信息

介绍 type 和 object 之间的恩怨纠葛时,我们说类对象的基类是在初始化的时候设置的,来看一下。

int
PyType_Ready(PyTypeObject *type)
{
    // 注意:参数 type 只是一个普通的 C 变量,和 Python 的 <class 'type'> 无关
    // dict:属性字典,即 __dict__,bases:继承的所有基类,即 __bases__
    PyObject *dict, *bases;
    // 继承的第一个基类,即 __base__
    PyTypeObject *base;
    Py_ssize_t i, n;

    // ...
    // 获取类型对象中 tp_base 字段指定的基类
    base = type->tp_base;
    // 如果基类为空、并且该类本身不是 <class 'object'>
    if (base == NULL && type != &PyBaseObject_Type) {
        // 那么将该类的基类设置为 <class 'object'>、即 &PyBaseObject_Type
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }

    // 如果基类不是 NULL,也就是指定了基类,但是基类的属性字典是 NULL
    // 说明该类的基类尚未初始化,那么会先对基类进行初始化
    // 注意这里的 tp_dict,它表示每个类都会有的属性字典
    // 而属性字典不等于 NULL,是类型对象初始化完成的重要标志
    if (base != NULL && base->tp_dict == NULL) {
        if (PyType_Ready(base) < 0)
            goto error;
    }
    // Py_TYPE 是一个宏,会返回对象的 ob_type
    // 如果该类型对象的 ob_type 为空,但是基类不为空(显然这里是针对于自定义类型对象)
    // 那么将该类型对象的 ob_type 设置为基类的 ob_type
    // 为什么要做这一步,我们后面会详细说
    if (Py_TYPE(type) == NULL && base != NULL)
        Py_TYPE(type) = Py_TYPE(base);
    // 获取 __bases__,检测是否为空
    bases = type->tp_bases;
    // 如果为空,则根据 __base__ 进行设置
    if (bases == NULL) {
        // 如果 base 也为空,说明当前的类对象一定是 <class 'object'>
        // 那么 bases 就是空元祖
        if (base == NULL)
            bases = PyTuple_New(0);
        // 如果 base 不为空,那么 bases 就是 (base,)
        else
            bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            goto error;
        // 设置 tp_bases
        type->tp_bases = bases;
    }
    // 设置属性字典,后续再聊
    dict = type->tp_dict;
    if (dict == NULL) {
        dict = PyDict_New();
        if (dict == NULL)
            goto error;
        type->tp_dict = dict;
    }
    // ...
}

对于指定了 tb_base 的类对象,当然就使用指定的基类,而对于没有指定 tp_base 的类对象,虚拟机会为其设置一个默认的基类:&PyBaseObject_Type ,也就是 Python 的 object。

所以对于 PyType_Type 而言,它的 tp_base 会指向 PyBaseObject_Type,这在 Python 中体现的就是 type 继承 object、或者说 object 是 type 的父类。但是所有的类的 ob_type 又都指向了 PyType_Type,包括 object,因此我们又说 type 是包括 object 在内的所有类对象的类对象(元类)。

而在获得了基类之后,会判断基类是否被初始化,如果没有,则需要先对基类进行初始化,而判断初始化是否完成的条件是 tp_dict 是否不等于 NULL。对于内置类对象来说,在解释器启动的时候,就已经作为全局对象存在了,所以它们的初始化不需要做太多工作,只需小小的完善一下即可,比如设置基类、类型、以及对 tp_dict 进行填充。

在基类设置完毕后,会继续设置 ob_type,而源码是这么设置的:Py_TYPE(type) = Py_TYPE(base),也就是将基类的 ob_type 设置成了当前类的 ob_type,那么这一步的意义何在呢?直接设置成 <class 'type'> 不就完了吗?

class MyType(type):
    pass

class A(metaclass=MyType):
    pass

class B(A):
    pass


print(type(A))  # <class '__main__.MyType'>
print(type(B))  # <class '__main__.MyType'>

我们看到 B 继承了 A,而 A 的类型是 MyType,那么 B 的类型也成了 MyType。也就是说 A 是由 XX 生成的,那么 B 在继承 A 之后,B 也会由 XX 生成,所以源码中的那一步就是用来做这件事情的。另外,这里之所以用 XX 代替,是因为 Python 里面不仅仅只有 type 是元类,那些继承了 type 的子类也可以是元类。

而且如果你熟悉 flask 的话,你会发现 flask 源码里面就有类似于这样的操作:

class MyType(type):
    def __new__(mcs, name, bases, attrs):
        # 关于第一个参数我们需要说一下,对于一般的类来说这里应该是 cls
        # 但这里是元类,所以应该用 mcs,意思就是 metaclass
        # 我们额外设置一些属性吧,关于元类后续会介绍
        # 虽然目前还没有看底层实现,但至少使用方法应该知道
        attrs.update({"name": "古明地觉"})
        return super().__new__(mcs, name, bases, attrs)

def with_metaclass(meta, bases=(object,)):
    return meta("", bases, {})

class Girl(with_metaclass(MyType, (int,))):
    pass


print(type(Girl))  # <class '__main__.MyType'>
print(getattr(Girl, "name"))  # 古明地觉
print(Girl("123"))  # 123

所以逻辑很清晰了,虚拟机就是将基类的 metaclass 设置为子类的 metaclass。以 PyType_Type 为例,其 metaclass 就是 object 的 metaclass,也是它自己。而在源码的 PyBaseObject_Type 定义中也可以看到,其 ob_type 被设置成了 &PyType_Type。

tb_base 和 ob_type 设置完毕之后,会设置 tb_bases。tb_base 对应 __base__,tb_bases 对应 __bases__,我们用 Python 演示一下,这两者的区别。

class A:
    pass

class B(A):
    pass

class C:
    pass

class D(B, C):
    pass

  
print(D.__base__)  # <class '__main__.B'>
print(D.__bases__)  # (<class '__main__.B'>, <class '__main__.C'>)
print(C.__base__)  # <class 'object'>
print(C.__bases__)  # (<class 'object'>,)
print(B.__base__)  # <class '__main__.A'>
print(B.__bases__)  # (<class '__main__.A'>,)

我们看到 D 同时继承多个类,那么 tp_base 就是先出现的那个基类。而 tp_bases 则是继承的所有基类,但是基类的基类是不会出现的,比如 object。然后我们看看 C,因为 C 没有显式地继承任何类,那么 tp_bases 就是 NULL。但是 Python3 里面所有的类都默认继承了object,所以 tp_base 就是 object,而 tp_bases 显然是 (object,)。

以上就是 tp_base、ob_type、tp_bases 的设置,还是比较简单的,它们在设置完毕之后,就要对 tp_dict 进行填充了。而填充 tp_dict 是一个极其繁复的过程,我们继续往下看。

类对象属性字典(tp_dict)的填充

在给类对象设置完基类、以及类型信息之后,就开始填充属性字典了,这是一个非常复杂的过程。

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 相关的操作加入到 tp_dict 中
    // 注意:这里的 type 是 PyType_Ready 的参数 type
    // 它可以是 Python 的 <class 'type'>、也可以是 <class 'int'>
    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) {  // 类似于 @property
        if (add_getset(type, type->tp_getset) < 0)
            goto error;
    }  
    // ...
}  

在这个阶段,完成了将魔法函数的函数名和函数体加入 tp_dict 的过程,里面的 add_operators 、 add_methods 、 add_members 、 add_getset 都是完成填充 tp_dict 的动作。

那么这时候一个问题就出现了,以整数的 __sub__ 为例,我们知道它会对应底层的 C 函数 long_sub,可虚拟机是如何知道 __sub__ 和 long_sub 之间存在关联的呢?其实这种关联显然是一开始就已经定好了的,存放在一个名为 slotdefs 的数组中。

slot 与操作排序

在进入填充 tp_dict 的复杂操作之前,我们先来看一个概念:slot。slot 可以视为 PyTypeObject 中定义的操作,一个魔法函数对应一个 slot,比如 __add__、__sub__ 等等,都会对应一个 slot。我们看看 slot 的底层结构,它是由 slotdef 这个结构体来实现的,内部除了函数指针之外,它还包含了其它信息。

// Objects/typeobject.c
typedef struct wrapperbase slotdef;

//Include/descrobject.h
struct wrapperbase {
    const char *name;
    int offset;
    void *function;
    wrapperfunc wrapper;
    const char *doc;
    int flags;
    PyObject *name_strobj;
};
// 从定义上看,slot 不是一个 PyObject

slot 中存储着 PyTypeObject 的操作对应的各种信息,并且 PyTypeObject 对象中的每一个操作都会有一个 slot 与之对应。然后是里面每个字段的含义:

  • name:暴露给 Python 的名称,比如 "__sub__"、"__str__" 等等。
  • offset:承载具体实现的 C 函数在 XXX 中的偏移量,至于这个 XXX 是什么,一会儿说。
  • function:承载具体实现的 C 函数。

这里又整出来一个 PyHeapTypeObject,它是做什么的,别着急,我们先来看看如何创建一个 slot。Python 在底层提供了多个宏,其中最基本的是 TPSLOT 和 ETSLOT。

// Objects/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)}

所以 slot 里面的 offset 字段表示承载具体实现的 C 函数在 PyTypeObject 或 PyHeapTypeObject 中的偏移量。

// Include/cpython/object.h
typedef struct _heaptypeobject {
    PyTypeObject ht_type;
    PyAsyncMethods as_async;
    PyNumberMethods as_number;
    PyMappingMethods as_mapping;
    PySequenceMethods as_sequence; 
    PyBufferProcs as_buffer;
    PyObject *ht_name, *ht_slots, *ht_qualname;
    struct _dictkeysobject *ht_cached_keys;
} PyHeapTypeObject;

这个 PyHeapTypeObject 是为自定义类对象准备的,它的第一个字段就是 PyTypeObject,至于其它的则是操作簇。至于为什么要有这么一个对象,原因是自定义类对象和相关的操作簇在内存中是连续的,必须在运行时动态分配内存,所以它是为自定义类准备的(具体细节后续剖析)。

那么这里就产生了一个问题,假设我们定义了一个类继承自 int,根据继承关系,显然自定义的类是具有 PyNumberMethods 这个操作簇的,它可以使用 __add__、__sub__ 之类的魔法函数。

但操作簇是定义在 PyTypeObject 里面的,而此时的 offset 却是基于 PyHeapTypeObject 得到的偏移量,那么通过这个 offset 显然无法准确找到操作簇里面的函数指针,比如 long_add、long_sub 等等。那我们要这个 offset 还有何用呢?答案非常诡异,这个 offset 是用来对操作进行排序的。排序?我整个人都不好了。

不过在理解为什么要对操作进行排序之前,需要先看看底层预定义的 slot 集合 slotdefs。

// Objects/typeobject.c
#define BINSLOT(NAME, SLOT, FUNCTION, DOC) \
    ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_l, \
           NAME "($self, value, /)\n--\n\nReturn self" DOC "value.")

#define RBINSLOT(NAME, SLOT, FUNCTION, DOC) \
    ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_r, \
           NAME "($self, value, /)\n--\n\nReturn value" DOC "self.")

#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)


static slotdef slotdefs[] = {
    // ...
  
    /* name = "__repr__"
     * offset = offsetof(PyTypeObject, tp_repr)
     * function = slot_tp_repr
     * wrapper = wrap_unaryfunc
     */
    TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc,
           "__repr__($self, /)\n--\n\nReturn repr(self)."),
  
    /* name = "__hash__"
     * offset = offsetof(PyTypeObject, tp_hash)
     * function = slot_tp_hash
     * wrapper = wrap_hashfunc
     */
    TPSLOT("__hash__", tp_hash, slot_tp_hash, wrap_hashfunc,
           "__hash__($self, /)\n--\n\nReturn hash(self)."),
    FLSLOT("__call__", tp_call, slot_tp_call, (wrapperfunc)(void(*)(void))wrap_call,
           "__call__($self, /, *args, **kwargs)\n--\n\nCall self as a function.",
           PyWrapperFlag_KEYWORDS),
    TPSLOT("__str__", tp_str, slot_tp_str, wrap_unaryfunc,
           "__str__($self, /)\n--\n\nReturn str(self)."),
    TPSLOT("__getattribute__", tp_getattro, slot_tp_getattr_hook,
           wrap_binaryfunc,
           "__getattribute__($self, name, /)\n--\n\nReturn getattr(self, name)."),
    // ...
  
    /* name = "__new__"
     * offset = offsetof(PyTypeObject, tp_new)
     * function = slot_tp_new
     * wrapper = NULL
     */
    TPSLOT("__new__", tp_new, slot_tp_new, NULL,
           "__new__(type, /, *args, **kwargs)\n--\n\n"
           "Create and return new object.  See help(type) for accurate signature."),
  
    /* name = "__del__"
     * offset = offsetof(PyTypeObject, tp_finalize)
     * function = slot_tp_finalize
     * wrapper = wrap_del
     */
    TPSLOT("__del__", tp_finalize, slot_tp_finalize, (wrapperfunc)wrap_del, ""),
  
    // ...
  
    /* name = "__add__"
     * offset = offsetof(PyHeapTypeObject, as_number.nb_add)
     * function = slot_nb_add
     * wrapper = wrap_binaryfunc_l
     */  
    BINSLOT("__add__", nb_add, slot_nb_add,
           "+"),
    /* name = "__radd__"
     * offset = offsetof(PyHeapTypeObject, as_number.nb_add)
     * function = slot_nb_add
     * wrapper = wrap_binaryfunc_r
     */  
    RBINSLOT("__radd__", nb_add, slot_nb_add,
           "+"),
    BINSLOT("__sub__", nb_subtract, slot_nb_subtract,
           "-"),
    RBINSLOT("__rsub__", nb_subtract, slot_nb_subtract,
           "-"),
    BINSLOT("__mul__", nb_multiply, slot_nb_multiply,
           "*"),
    RBINSLOT("__rmul__", nb_multiply, slot_nb_multiply,
           "*"),
    BINSLOT("__mod__", nb_remainder, slot_nb_remainder,
           "%"),
    RBINSLOT("__rmod__", nb_remainder, slot_nb_remainder,
           "%"),
    // ...
    /* name = "__getitem__"
     * offset = offsetof(PyHeapTypeObject, as_mapping.mp_subscript)
     * function = slot_mp_subscript
     * wrapper = wrap_binaryfunc
     */ 
    MPSLOT("__getitem__", mp_subscript, slot_mp_subscript,
           wrap_binaryfunc,
           "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
    
    // ...
    /* name = "__getitem__"
     * offset = offsetof(PyHeapTypeObject, as_sequence.sq_item)
     * function = slot_sq_item
     * wrapper = wrap_sq_item
     */ 
    SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item,
           "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
}

在 slotdefs 中可以发现,操作名和操作并不是一一对应的,存在多个操作对应同一个操作名、或者多个操作名对应同一个操作的情况。那么在填充 tp_dict 时,就会出现问题,比如 __getitem__,在 tp_dict 中与其对应的是 mp_subscript 还是 sq_item 呢?这两者都是通过 [] 进行操作的,比如字典根据 key 获取 value、列表基于索引获取元素,对应的都是 __getitem__。

为了解决这个问题,就需要利用 slot 中的 offset 信息对 slot(也就是操作)进行排序。回顾一下 PyHeapTypeObject 的定义,与一般的 struct 定义不同,它的各个字段的顺序是非常关键的,在顺序中隐含着操作优先级的问题。

在 PyHeapTypeObject 中,PyMappingMethods 的位置在 PySequenceMethods 之前,mp_subscript 是 PyMappingMethods 中的一个函数指针,而 sq_item 又是 PySequenceMethods 中的一个函数指针。那么最终计算出来的偏移量就存在如下关系:

offset(mp_subscript) < offset(sq_item)

因此如果在一个 PyTypeObject 中,既定义了 mp_subscript,又定义了 sq_item,那么虚拟机将选择 mp_subscript 与 __getitem__ 建立联系。我们举个栗子:

class A(list):

    def __getitem__(self, item):
        return item


a = A([])
print(a)  # []
print(a[0])  # 0
print(a["xxx"])  # xxx

我们自定义的类实现了 __getitem__,所以会对应 mp_subscript 或 sq_item,那么到底是哪一种呢?显然根据偏移量的关系,虚拟机最终选择了让 mp_subscript 和 __getitem__ 建立联系。

事实上不看偏移量我们也知道答案,因为 sq_item 表示基于索引取值,如果 [] 里面的值是字符串,那么铁定报错。但这里没有报错,说明和 __getitem__ 建立联系的不是 sq_item。

注:如果是针对内置类对象,则没有这么复杂,因为它们的操作在底层是静态写死的。但对于自定义类对象来说,需要有一个基于偏移量排序、查找的过程。

slot 变成 descriptor

看一下之前的一张图:

当时说 "__sub__" 对应的 value 并不是一个直接指向 long_sub 函数的指针,而是指向一个结构体,至于指向 long_sub 函数的指针则在该结构体内部。那么问题来了,这个结构体是不是上面的 slot 呢?

我们知道在 slot 中,包含了一个操作的相关信息。但是很可惜,在 tp_dict 中,与 "__sub__" 关联在一起的,一定不会是 slot,因为它不是一个 PyObject,无法将其指针放在字典中。如果再深入思考一下,会发现 slot 也无法被调用。因为它不是一个 PyObject,那么它就没有 ob_type 这个字段,也就无从谈起什么 tp_call 了,所以 slot 是无论如何也无法满足 Python 中可调用(callable)这一条件的。

前面我们说过,虚拟机在 tp_dict 中找到对应的操作后,会调用该操作,所以 tp_dict 中与 "__sub__" 对应的只能是包装了 slot 的 PyObject(的指针),我们称之为 wrapper descriptor。在 Python 内部存在多种 wrapper descriptor,它在底层对应的结构体为 PyWrapperDescrObject。

// Include/descrobject.h
typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

#define PyDescr_COMMON PyDescrObject d_common

typedef struct {
    // 相当于 PyDescrObject d_common
    PyDescr_COMMON;
    // slot
    struct wrapperbase *d_base;
    // 函数指针
    void *d_wrapped;
} PyWrapperDescrObject;

以上就是 wrapper descriptor 在底层的定义,一个 wrapper descriptor 包含一个 slot,其创建是通过 PyDescr_NewWrapper 完成的。

// Objects/descrobject.c
PyObject *
PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped)
{
    // 声明 wrapper descriptor 指针
    PyWrapperDescrObject *descr;
    // 调用 descr_new 申请内存
    descr = (PyWrapperDescrObject *)descr_new(&PyWrapperDescr_Type,
                                             type, base->name);
    // 设置字段属性
    if (descr != NULL) {
        descr->d_base = base;
        descr->d_wrapped = wrapped;
    }
    return (PyObject *)descr;
}

static PyDescrObject *
descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
{
    PyDescrObject *descr;
    // 为 PyDescrObject 申请内存
    descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
    // 设置字段属性
    if (descr != NULL) {
        Py_XINCREF(type);
        descr->d_type = type;
        descr->d_name = PyUnicode_InternFromString(name);
        if (descr->d_name == NULL) {
            Py_DECREF(descr);
            descr = NULL;
        }
        else {
            descr->d_qualname = NULL;
        }
    }
    return descr;
}

Python 内部的各种 wrapper descriptor 都会包含 PyDescrObject,也就是类型对象相关的一些信息;d_base 对应 slot;而 d_wrapped 则存放着最重要的东西:操作对应的函数指针,比如 PyLong_Type,其 tp_dict["__sub__"].d_wrapped 就是 &long_sub。

print(int.__sub__)
print(str.__add__)
print(str.__getitem__)
print(tuple.__hash__)
"""
<slot wrapper '__sub__' of 'int' objects>
<slot wrapper '__add__' of 'str' objects>
<slot wrapper '__getitem__' of 'str' objects>
<slot wrapper '__hash__' of 'tuple' objects>
"""

我们看到这些魔法函数都是一个 wrapper descriptor 对象,也就是对 slot 包装之后的描述符。wrapper descriptor 对象在底层对应 PyWrapperDescrObject,其类型是 PyWrapperDescr_Type,tp_call 为 wrapperdescr_call。

print(int.__sub__.__class__)
print(str.__add__.__class__)
print(str.__getitem__.__class__)
print(tuple.__hash__.__class__)
"""
<class 'wrapper_descriptor'>
<class 'wrapper_descriptor'>
<class 'wrapper_descriptor'>
<class 'wrapper_descriptor'>
"""
# int.__sub__ 等价于 int.__dict__["__sub__"]
print(int.__dict__["__sub__"].__class__)
"""
<class 'wrapper_descriptor'>
"""

打印的结果是 <class 'wrapper_descriptor'>,说明类型对象 wrapper_descriptor 在底层对应 PyWrapperDescr_Type。

所以内置类对象的属性字典中存储的是字符串到 wrapper descriptor 的映射。

建立联系

slotdefs 里面包含了一堆 slot,每个 slot 对应类型对象定义的一个操作,比如 __getattr__、__new__、__add__、__getitem__ 等等。当然啦,虚拟机还会对 slotdefs 进行排序,排序之后再从头到尾遍历 slotdefs,基于每个 slot 创建一个 wrapper descriptor。然后在 tp_dict 中再建立从操作名到 wrapper descriptor 的映射,这个过程是在 add_operators 中完成的。

我们看一下 add_operators 的逻辑。

static int slotdefs_initialized = 0;

static void
init_slotdefs(void)
{
    slotdef *p;
    if (slotdefs_initialized)
        return;
    for (p = slotdefs; p->name; p++) {
        assert(!p[1].name || p->offset <= p[1].offset);
        // slot 有一个 name 字段和一个 name_strobj 字段
        // 它们都是暴露给 Python 的操作名,只不过一个是 C 字符串,一个是 Python 字符串
        // 基于 C 字符串创建 Python 字符串
        p->name_strobj = PyUnicode_InternFromString(p->name);
        if (!p->name_strobj || !PyUnicode_CHECK_INTERNED(p->name_strobj))
            Py_FatalError("Out of memory interning slotdef names");
    }
    // 该操作只会执行一次
    slotdefs_initialized = 1;
}

static int
add_operators(PyTypeObject *type)
{
    // 属性字典
    PyObject *dict = type->tp_dict;
    // slot,在底层是一个 slotdef 结构体
    slotdef *p;
    // wrapper descriptor
    PyObject *descr;
    void **ptr;
    // 而 init_slotdefs 就是基于 C 字符串创建 Python 字符串
    // p->name_strobj = PyUnicode_InternFromString(p->name);
    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_GetItemWithError(dict, p->name_strobj))
            continue;
        if (PyErr_Occurred()) {
            return -1;
        }
        if (*ptr == (void *)PyObject_HashNotImplemented) {
            if (PyDict_SetItem(dict, p->name_strobj, Py_None) < 0)
                return -1;
        }
        else {
            // 创建 wrapper descriptor
            descr = PyDescr_NewWrapper(type, p, *ptr);
            if (descr == NULL)
                return -1;
            // 将 "操作名": wapper descriptor 放入 tp_dict 中
            if (PyDict_SetItem(dict, p->name_strobj, descr) < 0) {
                Py_DECREF(descr);
                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 数组,通过 slotptr 获取该 slot 对应的操作在 PyTypeObject 中的函数指针。紧接着创建 wrapper descriptor,然后在 tp_dict 中建立从操作名(slotdef.name_strobj)到操作(wrapper descriptor)的映射。

但需要注意的是,在创建 wrapper descriptor 之前,虚拟机会检查在 tp_dict 中是否存在同名操作,如果存在了,则不会再次建立从操作名到操作的关联。也正是这种检查机制与排序机制相结合,虚拟机才能在拥有相同操作名的多个操作中选择优先级最高的操作。

add_operators 里面的大部分动作都很简单、直观,而最难的动作隐藏在 slotptr 这个函数当中,它的功能是完成从 slotslot 对应操作的真实函数指针的转换。我们知道在 slot 中存放着用来操作的 offset,但不幸的是,对于自定义类的操作簇来说,这个 offset 是相对于 PyHeapTypeObject 的偏移,而操作的真实函数指针却是在 PyTypeObject 中指定的。

此外 PyTypeObject 和 PyHeapTypeObject 也不是同构的,因为 PyHeapTypeObject 中包含了 PyNumberMethods 结构体,但 PyTypeObject 只包含了 PyNumberMethods * 指针。所以 slot 中存储的关于操作的 offset 对 PyTypeObject 来说,不能直接用,必须先转换。

举个栗子,假如有以下调用(slotptr 一会说):

slotptr(&PyLong_Type, offset(PyHeapTypeObject, long_sub))

首先会判断这个偏移量是否大于 offset(PyHeapTypeObject, as_number),所以会从 PyHeapTypeObject 对象中获取 as_number 字段的指针 p,然后在 p 的基础上进行偏移就可以得到实际的函数地址。所以偏移量 delta 为:

offset(PyHeapTypeObject, long_sub) - offset(PyHeapTypeObject, as_number)

而这个复杂的过程就在 slotptr 中完成:

static void **
slotptr(PyTypeObject *type, int ioffset)
{
    char *ptr;
    long offset = ioffset;

    /* Note: this depends on the order of the members of PyHeapTypeObject! */
    assert(offset >= 0);
    assert((size_t)offset < offsetof(PyHeapTypeObject, as_buffer));
    // 从 PyHeapTypeObject 中排在后面的 PySequenceMethods 开始判断
    // 然后向前,依次判断 PyMappingMethods 和 PyNumberMethods
    /*
     * 为什么要这么做呢?假设我们首先从 PyNumberMethods 开始判断
     * 如果一个操作的 offset 大于 as_numbers 在 PyHeapTypeObject 中的偏移量
     * 那么我们还是没办法确认这个操作到底是属于谁的
     * 只有从后往前进行判断,才能解决这个问题。
     */ 
    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 if ((size_t)offset >= offsetof(PyHeapTypeObject, as_async)) {
        ptr = (char *)type->tp_as_async;
        offset -= offsetof(PyHeapTypeObject, as_async);
    }
    else {
        ptr = (char *)type;
    }
    if (ptr != NULL)
        ptr += offset;
    return (void **)ptr;
}

好了,到现在我们应该能够摸清楚虚拟机在改造 PyTypeObject 对象时,对 tp_dict 做了什么了,我们以 PyLong_Type 举例说明:

在 add_operators 完成之后,PyLong_Type 如图所示。

从 PyLong_Type.tp_as_number 中延伸出去的部分是在编译时就已经确定好了的,而从 tp_dict 中延伸出去的部分则是在 Python 运行时环境初始化的时候才建立的。这个运行时环境初始化后面会单独说,现在就把它理解为解释器启动时做的准备工作即可。

另外, PyType_Ready 在通过 add_operators 添加了 PyTypeObject 中定义的一些 operator 后,还会通过 add_methods、add_numbers 和 add_getsets 添加 PyTypeObject 中定义的 tp_methods、tp_members 和 tp_getset 函数集。

这些过程和 add_operators 类似,不过最后添加到 tp_dict 中的就不再是 PyWrapperDescrObject ,而分别是 PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject 。

print(int.__add__)
print((123).__add__)
"""
<slot wrapper '__add__' of 'int' objects>
<method-wrapper '__add__' of int object at 0x100be5030>
"""

print(int.__add__.__class__)
print((123).__add__.__class__)
"""
<class 'wrapper_descriptor'>
<class 'method-wrapper'>
"""

实例在调用函数的时候,会将函数包装成方法,它是一个 wrapper method。所以 __add__ 对于 int 类型对象而言,叫魔法函数,对于整数对象而言,叫魔法方法。

// 像 str.__add__、int.__sub__,它们都是 wrapper descriptor
// 在底层对应 PyWrapperDescrObject 结构体实例

// 而像 "hello".__add__、(123).__sub__,它们都是 wrapper method
// 在底层对应 wrapperobject 结构体实例
// 但是我们看到 wrapperobject 只是多了一个 self 而已
// 所以 "hello".upper() 等价于 str.upper("hello")
typedef struct {
    PyObject_HEAD
    PyWrapperDescrObject *descr;
    PyObject *self;
} wrapperobject;
// 关于函数和方法的区别后续还会细说

从目前来看,基本上算是解析完了,但是还有一点:

class A(int):
    def __sub__(self, other):
        return self, other

a = A(123)
print(a - 456)  # (123, 456)

从结果上很容易看出,进行减法操作时,调用的是我们重写的 __sub__。这意味着虚拟机在初始化 A 的时候,对 tp_as_number 中的 nb_subtract 进行了特殊处理。那为什么虚拟机会知道要对 nb_subtract 进行特殊处理呢?当然肯定有小伙伴会说:这是因为我们重写了 __sub__ 啊,确实如此,但这是 Python 层面上的,如果站在虚拟机层面的话,答案还是在 slot 身上。

虚拟机在初始化类对象 A 时,会检测出 A 的 tp_dict 中存在 __sub__。在后面剖析自定义类对象的创建时会看到,因为在定义 class A 的时候,重写了 __sub__ 这个操作,所以在 A 的 tp_dict 中,__sub__ 一开始就会存在,虚拟机会检测到。

然后再根据 __sub__ 对应的 slot 顺藤摸瓜,找到 nb_substract,并且将这个函数指针替换为 slot 中指定的 &slot_nb_subtract。所以当后来虚拟机找 A 的 nb_substract 的时候,实际上找到的是 slot_nb_subtract。而在 slot_nb_subtract 中,会寻找 __sub__ 对应的描述符,然后找到在 A 中重写的函数(一个 PyFunctionObject *)。这样一来,就完成了对 int 的 __sub__ 行为的替换。

所以对于 A 来说,内存布局就是下面这样。

当然这仅仅是针对于 __sub__,至于其它操作还是会指向 PyLong_Type 中指定的函数。所以如果某个函数在 A 里面没有重写的话,那么会从 PyLong_Type 中寻找。

以上就是属性字典的填充,这个过程还是稍微有点复杂的。

类对象 MRO 的设置与属性继承

当完成属性字典的设置,就开始确定 MRO 了,即 method resolve order。

类对象 MRO 的设置

MRO 表示类继承之后,属性或方法的查找顺序。如果 Python 是单继承的话,那么这不是问题,直接一层一层向上找即可。但 Python 是支持多继承的,那么在多继承时,继承的顺序就成为了一个必须考虑的问题。

class A:
    def foo(self):
        print("A")

class B(A):
    def foo(self):
        print("B")

class C(A):
    def foo(self):
        print("C")
        self.bar()

    def bar(self):
        print("bar C")

class D(C, B):
    def bar(self):
        print("bar D")

d = D()
d.foo()
"""
C
bar D
"""

首先打印的是字符串 "C",表示调用的是 C 的 foo,说明把 C 写在前面,会先从 C 里面查找。但是下面打印了 "bar D",这是因为 C 里面的 self 实际上是 D 的实例对象。

因为 D 在找不到 foo 函数的时候,会到父类里面找,但是同时也会将 self 传递过去。调用 self.bar 的时候,这个 self 是 D 的实例对象,所以还是会先到 D 里面找,如果找不到再去父类里面找。

而对于虚拟机而言,则是会在 PyType_Ready 中通过 mro_internal 函数确定 mro。虚拟机将创建一个 PyTupleObject 对象,里面存放一组类对象,这些类对象的顺序就是虚拟机确定的 mro。而这个元组,最终会被交给 tp_mro 字段保存。

由于确定 MRO 的 mro_internal 函数非常复杂,这里我们就不看源码了,只要能从概念上理解它即可。另外 Python 早期有经典类和新式类两种,现在则只存在新式类,而经典类和新式类采用的搜索策略是不同的,举个例子:

图中的箭头表示继承关系,比如:A 同时继承 B 和 C、B 继承 D、C 继承 E。

对于上图来说,经典类和新式类的查找方式是一样的,至于两边是否一样多则不重要。查找方式是先从 A 找到 I,再从 C 查找到 G。我们实际演示一下,由于经典类只在 Python2 中存在,所以下面只演示新式类。

I = type("I", (), {})
H = type("H", (I,), {})
F = type("F", (H,), {})
G = type("G", (), {})
D = type("D", (F,), {})
E = type("E", (G,), {})
B = type("B", (D,), {})
C = type("C", (E,), {})
A = type("A", (B, C), {})

for _ in A.__mro__:
    print(_)

"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.I'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class 'object'>
"""

A 继承两个类,然后这两个类分别继续继承,如果最终没有继承公共的类(忽略 object),那么经典类和新式类是一样的。像这种泾渭分明、各自继承各自的,都是先一条路找到黑,然后再去另外一条路找。

但如果是下面这种,分久必合、两者最终又继承了同一个类,那么经典类还是跟以前一样,按照每一条路都走到黑的方式。但是对于新式类,则是先从 A 找到 H,而 I 这个两边最终都继承的类不找了,然后从 C 找到 I,也就是在另一条路找到头。

我们测试一下:

# 新式类
I = type("I", (), {})
H = type("H", (I,), {})
F = type("F", (H,), {})
G = type("G", (I,), {})   # 这里让 G 继承 I
D = type("D", (F,), {})
E = type("E", (G,), {})
B = type("B", (D,), {})
C = type("C", (E,), {})
A = type("A", (B, C), {})

for _ in A.__mro__:
    print(_)

"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class '__main__.I'>
<class 'object'>
"""

但是 Python 的多继承比我们想象的要复杂,原因就在于可以任意继承,如果 B 和 C 再分别继承两个类呢?那么我们这里的线路就又要多出两条了。不过既然要追求刺激,就贯彻到底喽,我们来看一下,如何从混乱不堪的继承关系中,找到正确的继承顺序。

由于 Python3 只有新式类,因此下面我们会以介绍新式类为主,经典类了解一下即可。

很多文章可能告诉你经典类采用深度优先算法,新式类采用广度优先算法,真的是这样吗?我们举一个例子:

假设我们调用 A() 的 foo 方法,但是 A 里面没有,那么理所应当会去 B 里面找。但是 B 里面也没有,而 C 和 D 里面有,那么这个时候是去 C 里面找还是去 D 里面找呢?根据我们之前的结论,显然是去 D 里面找,可如果按照广度优先的逻辑来说,那么应该是去 C 里面找啊。所以广度优先理论在这里就不适用了,因为 B 继承了 D,而 B 和 C 并没有直接关系,我们应该把 B 和 D 看成一个整体。

而 Python 的 MRO 实际上是采用了一种叫做 C3 的算法,这个 C3 算法比较复杂(其实也不算复杂),但是我个人总结出一个更加好记的结论,如下:

当沿着一条继承链寻找类时,默认会沿着该继承链一直找下去。但如果发现某个类出现在了另一条继承链当中,那么当前的继承链的搜索就会结束,然后在"最开始"出现分歧的地方转向下一条继承链的搜索。

这是我个人总结的,或许光看字面意思的话会比较难理解,但是通过例子就能明白了。

箭头表示继承关系,继承顺序是从左到右,比如这里的 A 就相当于 class A(B, C),下面我们来从头到尾分析一下 A 的 MRO。

  • 1)因为是 A 的 MRO,所以查找时,第一个类就是 A;
  • 2)然后 A 继承 B 和 C,由于是两条路,因此我们说 A 这里就是一个分歧点。但由于 B 在前,所以接下来是 B,而现在 MRO 的顺序就是 A B;
  • 3)但是 B 这里也出现了分歧点,不过不用管,因为我们说会沿着继承链不断往下搜索,现在 MRO 的顺序是A B D;
  • 4)然后从 D 开始继续寻找,这里注意了,按理说会找到 G 的。但是 G 不止被一个类继承,也就是说沿着当前的继承链查找 G 时,发现 G 还出现在了其它的继承链当中。怎么办?显然要回到最初的分歧点,转向下一条继承链的搜索;
  • 5)最初的分歧点是 A,那么该去找 C 了,现在 MRO 的顺序就是 A B D C;
  • 6)注意 C 这里也出现了分歧点,而 A 的两条分支已经结束了,所以现在 C 就是最初的分歧点了。而 C 继承自 E 和 F,显然要搜索 E,那么此时 MRO 的顺序就是 A B D C E;
  • 7)然后从 E 开始搜索,显然要搜索 G,此时 MRO 顺序变成 A B D C E G;
  • 8)从 G 要搜索 I,此时 MRO 的顺序是 A B D C E G I;
  • 9)从 I 开始搜索谁呢?由于 J 出现在了其它的继承链中,那么要回到最初的分歧点,也就是 C。那么下面显然要找 F,此时 MRO 的顺序是 A B D C E G I F;
  • 10)F 只继承了 H,那么肯定要找 H,此时 MRO 的顺序是 A B D C E G I F H;
  • 11)H 显然只能找 J 了,因此最终 A 的 MRO 的顺序就是 A B D C E G I F H J object;

我们实际测试一下:

J = type("J", (object, ), {})
I = type("I", (J, ), {})
H = type("H", (J, ), {})
G = type("G", (I, ), {})
F = type("F", (H, ), {})
E = type("E", (G, H), {})
D = type("D", (G, ), {})
C = type("C", (E, F), {})
B = type("B", (D, E), {})
A = type("A", (B, C), {})

# A B D C E G I F H J
for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.G'>
<class '__main__.I'>
<class '__main__.F'>
<class '__main__.H'>
<class '__main__.J'>
<class 'object'>
"""

为了加深理解,我们再举个更复杂的例子:

  • 1)首先是 A,A 继承 B1、B2、B3,会先走 B1,此时 MRO 是 A B1。并且现在 A 是分歧点;
  • 2)从 B1 处本来应该去找 C1,但是 C1 还被其它类继承,也就是出现在了其它的继承链当中。因此要回到最初的分歧点 A,从下一条继承链开始找,显然要找 B2,此时 MRO 就是 A B1 B2;
  • 3)从 B2 开始,显然要找 C1,此时 MRO 的顺序就是 A B1 B2 C1;
  • 4)从C1开始,显然要找 D1,因为 D1 只被 C1 继承,也就是说,它没有出现在另一条继承链当中,因此此时 MRO 的顺序是 A B1 B2 C1 D1;
  • 5)对于 D1 而言,显然接下来是不会去找 E 的,因为 E 还出现在另外的继承链当中。咋办? 回到最初的分歧点,注意这里的分歧点还是 A,因为 A 的分支还没有走完。显然此时要走 B3,那么 MRO 的顺序就是 A B1 B2 C1 D1 B3;
  • 6)从 B3 开始找,显然要找 C2,注意:A 的分支已经走完,此时 B3 就成了新的最初分歧点。现在 MRO 的顺序是 A B1 B2 C1 D1 B3 C2;
  • 7)C2 会找 D2吗?显然不会,因为 D2 还被 C3 继承,所以它出现在了其它的继承链中。于是要回到最初的分歧点,这里是 B3,显然下面要找 C3。另外由于 B3 的分支也已经走完,所以现在 C3 就成了新的最初分歧点。此时 MRO 的顺序是 A B1 B2 C1 D1 B3 C2 C3;
  • 8)从 C3 开始,显然要找 D2,此时 MRO 的顺序是 A B1 B2 C1 D1 B3 C2 C3 D2;
  • 9)但是 D2 不会找 E,因此回到最初分歧点 C3,下面要找 D3。而 D3 找完之后显然只能再找 E 了,因此最终 MRO 的顺序是 A B1 B2 C1 D1 B3 C2 C3 D2 D3 E object;

下面测试一下:

E = type("E", (), {})
D1 = type("D1", (E,), {})
D2 = type("D2", (E,), {})
D3 = type("D3", (E,), {})
C1 = type("C1", (D1, D2), {})
C2 = type("C2", (D2,), {})
C3 = type("C3", (D2, D3), {})
B1 = type("B1", (C1,), {})
B2 = type("B2", (C1, C2), {})
B3 = type("B3", (C2, C3), {})
A = type("A", (B1, B2, B3), {})

for _ in A.__mro__:
    print(_)
"""
<class '__main__.A'>
<class '__main__.B1'>
<class '__main__.B2'>
<class '__main__.C1'>
<class '__main__.D1'>
<class '__main__.B3'>
<class '__main__.C2'>
<class '__main__.C3'>
<class '__main__.D2'>
<class '__main__.D3'>
<class '__main__.E'>
<class 'object'>
"""

以上就是计算 MRO 所采用的策略,关于源码部分我们就不看了,复杂是一方面,重点是没什么太大必要。个人觉得,关于多继承从目前这个层面上来理解已经足够了。另外通过以上可以看出,Python 支持非常复杂的继承关系。但是实际使用多继承的时候,我们最好还是要斟酌一下,因为多继承如果设计的不好,容易使得类的继承关系变得非常混乱。

为此,Python 提供了一种模式叫做 Mixin,可以网上搜索一下。比较简单,这里就不展开了。

self 在多继承里面的一些坑

在执行父类函数时传入的 self 参数,是很多初学者容易犯的错误。

class A:
    def foo(self):
        print("A: foo")
        self.bar()

    def bar(self):
        print("A: bar")


class B:
    def bar(self):
        print("B: bar")


class C(A, B):
    def bar(self):
        print("C: bar")


C().foo()
"""
A: foo
C: bar
"""

C 的实例对象在调用 foo 的时候,首先会去 C 里面查找,但是 C 没有,所以按照 MRO 的顺序会去 A 里面找。而 A 里面存在,所以调用,但是调用时传递的 self 是 C 的实例对象,因为是 C 的实例对象调用的。所以里面的 self.bar,这个 self 还是 C 的实例对象,那么调用 bar 的时候,会去哪里找呢?显然还是从 C 里面找,所以 self.bar() 的时候打印的是字符串 C: bar,而不是 A: bar

同理再来看一个关于 super 的栗子:

class A:
    def foo(self):
        super(A, self).foo()


class B:
    def foo(self):
        print("B: foo")


class C(A, B):
    pass


try:
    A().foo()
except Exception as e:
    print(e)  # 'super' object has no attribute 'foo'

# 首先 A 的父类是 object
# 所以 super(A, self).foo() 的时候会去执行 object 的 foo
# 而 object 没有 foo,所以报错


# 但如果是 C 的实例调用呢?
C().foo()  # B: foo

如果是 C() 调用 foo 的话,最终却执行了 B 的 foo,这是什么原因呢?首先 C 里面没有 foo,那么会去执行 A 的 foo,但是执行时候的 self 是 C 的实例对象,super 里面的 self 也是 C 里面的 self。

然后我们知道对于 C 而言,其 MRO 是 C、A、B、object。所以此时的 super(A, self).foo() 就表示:沿着 C 的 MRO 去找 foo 函数,但是 super 里面有一个 A,表示不要从头开始找,而是从 A 的后面开始找,所以下一个就找到 B 了。

所以说 super 不一定就是父类,而是要看里面的 self 是谁。总之:super(xxx, self) 一定是 type(self) 对应的 MRO 中,xxx 的下一个类。再举个栗子:

class A:
    def foo(self):
        # 从 B 里面找 foo
        super(A, self).foo()
        # 从 C 里面找 foo
        super(B, self).foo()


class B:
    def foo(self):
        print("B: foo")


class C:
    def foo(self):
        print("C: foo")


class D(A, B, C):
    pass



D().foo()
"""
B: foo
C: foo
"""

当 D() 调用 foo 的时候,D 里面没有,那么按照继承关系一定是去 A 里面找。而 self 是 D 的 self,那么 super(A, self) 就是 D 对应的 MRO 中,A 的下一个类,也就是 B;super(B, self) 就是 D 对应的 MRO 中,B 的下一个类,也就是 C。

基类冲突

虽然可以继承多个类,但是这些类之间有可能发生冲突,举个栗子:

class A(int, str):
    pass
# TypeError: multiple bases have instance lay-out conflict

我们发现执行的时候报错了,这个类虚拟机压根就不会让你创建,原因很简单,它们的实例对象在内存布局上发生冲突了。

比如 A("123") 这个实例,虚拟机是把它看成整数呢?还是字符串呢?因为 A 同时继承 int 和 str,就意味着其实例既可以像整数一样使用除法,也可以像字符串一样调用 join 方法。但是整数没有 join,字符串也无法使用除法,因此这个类是不合理的。继承的多个基类,在实例对象的内存布局上产生了冲突。

虽然上面给出了解释,但如果仔细推敲,会发现这个解释有些站不住脚,因为我们自定义的类没有这个问题。如果是自定义的类,别说继承两个,就算继承十个毫无关系的类,也不会出现冲突,这又是什么原因呢?其实两者的差别,就在于内置类对象的实例对象是没有属性字典的,但是自定义类对象的实例对象有。

class Base1:
    __slots__ = ("a",)


class Base2:
    __slots__ = ("a",)


class A(Base1, Base2):
    pass
# TypeError: multiple bases have instance lay-out conflict

此时 Base1 和 Base2 的实例,和内置类对象的实例是类似的,都没有属性字典,或者说实例对象可以绑定哪些属性已经定好了。同时继承 Base1 和 Base2 也会发生冲突。

结论:对于实例对象没有属性字典的类对象,最多同时继承一个。但是也有特例,如果类对象不接收任何参数,即使它的实例对象没有属性字典,也没有影响。

class Base1:
    __slots__ = ("a",)


class Base2:
    __slots__ = ()


# 合法
class A(Base2, int):
    pass


# 不合法
class B(Base1, int):
    pass

Base1 的实例对象显然可以绑定一个属性 a,意味着 Base1 可以接收一个参数;Base2 的实例对象无法绑定任何属性,意味着 Base2 不接收任何参数。

因此 class A(Base1, int) 是不合法的,因为 Base1 和 int 的实例没有属性字典,但它们都接收参数。同理 class A (Base1, str)class A(Base1, dict) 也一样。但是 class A(Base2, str) 没有影响,因为 Base2 的实例无法绑定任何属性,因此也就不存在内存布局上的冲突。

当继承的基类之间存在父子关系时

看个栗子:

class Base:
    pass


class A(Base):
    pass


class B(Base, A):
    pass
"""
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Base, A
"""

这个错误的原因应该不难理解,B 继承了 Base 和 A,但是 Base 和 A 之间又是父子关系。比如 B 在找不到某个方法时,会到 Base 里面找,Base 找不到再去 A 里面找。但如果 A 再找不到,就又回到了 Base,因为 A 继承 Base,那么此时就出现了闭环,这是不允许的。

但如果把 B 的创建改成 class B(A, Base) 则没问题,所以当继承的多个类之间也存在继承关系时,子类要在父类的前面。当然啦,最正确的做法应该是只继承子类,比如这里的 class B(A)。因为 A 已经继承了 Base,所以 B 只需继承 A 即可,无需再继承 Base。像 class B(A, Base) 这种做法虽然不会报错,但明显有些画蛇添足。

继承基类操作

子类在找不到某个属性或方法时,会去基类里面找,但这背后的原理是什么呢?答案简单到超乎你想象,就是单纯的拷贝。虚拟机在确定 MRO 之后,就会遍历 MRO 顺序列表,里面存储了该类的所有直接基类、间接基类(也就是基类的基类)。而虚拟机会将自身没有、但是基类中存在的操作拷贝到该类当中,从而完成对基类操作的继承动作。

而这个继承的动作是发生在 inherit_slots 中。

int
PyType_Ready(PyTypeObject *type)
{
    // 计算 MRO
    if (mro_internal(type, NULL) < 0)
        goto error;

    // inherit_special 会从基类继承 tp_new、tp_traverse、tp_clear 等
    if (type->tp_base != NULL)
        inherit_special(type, type->tp_base);

    // 获取 MRO 顺序列表
    bases = type->tp_mro;
    assert(bases != NULL);
    assert(PyTuple_Check(bases));
    n = PyTuple_GET_SIZE(bases);
    // 遍历所有的基类,包括直接基类、间接基类
    // 由于 MRO 的第一个类是其本身,所以遍历是从第二项开始的
    for (i = 1; i < n; i++) {
        PyObject *b = PyTuple_GET_ITEM(bases, i);
        if (PyType_Check(b))
            // 继承基类操作
            inherit_slots(type, (PyTypeObject *)b);
    }
    // ...
}    

在 inherit_slots 中会拷贝相当多的操作,这里就以整型为例:

static void
inherit_slots(PyTypeObject *type, PyTypeObject *base)
{
    PyTypeObject *basebase;

#undef SLOTDEFINED
#undef COPYSLOT
#undef COPYNUM
#undef COPYSEQ
#undef COPYMAP
#undef COPYBUF

#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 COPYASYNC(SLOT) COPYSLOT(tp_as_async->SLOT)
#define COPYNUM(SLOT) COPYSLOT(tp_as_number->SLOT)
#define COPYSEQ(SLOT) COPYSLOT(tp_as_sequence->SLOT)
#define COPYMAP(SLOT) COPYSLOT(tp_as_mapping->SLOT)
#define COPYBUF(SLOT) COPYSLOT(tp_as_buffer->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);
        COPYNUM(nb_subtract);
        COPYNUM(nb_multiply);
        COPYNUM(nb_remainder);
        COPYNUM(nb_divmod);
        COPYNUM(nb_power);
        COPYNUM(nb_negative);
        COPYNUM(nb_positive);
        COPYNUM(nb_absolute);
        COPYNUM(nb_bool);
        COPYNUM(nb_invert);
        COPYNUM(nb_lshift);
        COPYNUM(nb_rshift);
        COPYNUM(nb_and);
        COPYNUM(nb_xor);
        COPYNUM(nb_or);
        COPYNUM(nb_int);
        COPYNUM(nb_float);
        COPYNUM(nb_inplace_add);
        COPYNUM(nb_inplace_subtract);
        COPYNUM(nb_inplace_multiply);
        COPYNUM(nb_inplace_remainder);
        COPYNUM(nb_inplace_power);
        COPYNUM(nb_inplace_lshift);
        COPYNUM(nb_inplace_rshift);
        COPYNUM(nb_inplace_and);
        COPYNUM(nb_inplace_xor);
        COPYNUM(nb_inplace_or);
        COPYNUM(nb_true_divide);
        COPYNUM(nb_floor_divide);
        COPYNUM(nb_inplace_true_divide);
        COPYNUM(nb_inplace_floor_divide);
        COPYNUM(nb_index);
        COPYNUM(nb_matrix_multiply);
        COPYNUM(nb_inplace_matrix_multiply);
    }
    // ...
}    

我们在里面看到了很多熟悉的东西,这些都是在继承时需要拷贝的操作。比如布尔类型,PyBool_Type 中并没有设置 nb_add,但是 PyLong_Type 中设置了,而 bool 继承 int。所以布尔值是可以直接进行运算的,当然和整数、浮点数运算也是可以的。因此在 Numpy 中,判断一个数组里面有多少个满足条件的元素,可以使用 Numpy 提供的向量化操作进行比较,会得到一个具有相同长度的数组,里面的每一个元素为表示是否满足条件的布尔值。然后直接通过 sum 运算即可,因为运算的时候,True 会被解释成 1,False 会被解释成 0。

import numpy as np

arr = np.array([2, 4, 7, 3, 5])

print(arr > 4)  # [False False  True False  True]
print(np.sum(arr > 4))  # 2
# 也可以和整数直接运算
print(2.2 + True)  # 3.2

所以在 Python 中,整数可以和布尔值进行运算,看似不可思议,但又在情理之中。

填充基类的子类列表

到这里,PyType_Ready 还剩下最后一个重要的动作:设置基类的子类列表。

在 PyTypeObject 中,有一个 tp_subclasses,这个字段在 PyType_Ready 完成之后,将会是一个 list 对象,其中存放着所有直接继承该类的类对象。

class Base:
    pass

class A(Base):
    pass

class B(A):
    pass

# A 继承 Base
print(Base.__subclasses__())
"""
[<class '__main__.A'>]
"""

tp_subclasses 对应 Python 的 __subclasses__,当调用的时候会打印直接继承自己的类。注意是直接继承,间接继承不算,所以上面打印的列表里面只有 A 没有 B。那么问题来了,这一步是何时发生的呢?Base 怎么知道 A 继承了它呢?很简单,A 在继承 Base 的操作之后,也会将自身设置到 Base 的 tp_subclasses 中。

而在 PyType_Ready 中,这一步是通过调用 add_subclass 实现的。

int
PyType_Ready(PyTypeObject *type)
{
    // ...
    // 拿到所有的基类
    bases = type->tp_bases;
    n = PyTuple_GET_SIZE(bases);
    // 挨个遍历,将自身加入到基类的 tp_subclasses 中
    // 因为一个类可以直接继承很多基类,而每个基类都要添加
    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;
    }
    // ...
}    

当然啦,一个类可以继承多个基类,一个基类也可以被多个子类继承,比如 object。

print(object.__subclasses__())

打印会输出非常多的内容,因为 Python 里面所有的类都继承 object,而在创建这些类的时候,都会将自身加入到 object 的 tp_subclasses 里面。

小结

到这里,我们算是完整地剖析了 PyType_Ready 的动作,可以看到,虚拟机对类对象进行了多种繁杂的改造工作,主要包括以下几部分:

  • 设置类型信息、基类以及基类列表;
  • 填充属性字典;
  • 确定 MRO 顺序列表;
  • 基于 MRO 顺序列表从基类继承操作;
  • 设置基类的子类列表,或者说,将自身加入到基类的 tp_subclasses 中;

除了以上这些,还会继承 tp_as_number 等方法簇。

不同的类型,有些操作也会有一些不同的行为,但整体是一致的。因此具体到某个特定类型,可以自己跟踪 PyType_Ready 的具体过程。


 

欢迎大家关注我的公众号:古明地觉的编程教室。

如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。