Python内核阅读(十五): 类机制 (上)

Python 2017-08-17

起步

考虑如下的python代码:

class A(object):
    pass
a = A()

这其中包含了三个对象:object(class 对象), A(class 对象) 和 a(instance 对象). object 与 A之间存在父子关系(is-kind-of), 即A是object的子类. 而a和A之间的关系是实例的关系(is-instance-of). 从继承的关系上说, a也是object的实例.

python提供了 __class__ 属性或type方法 探测对象之间的is-instance-of关系. 而通过类的 __bases__ 属性则可以探测类之间的is-kind-of关系.

python还提供了内置方法 issubclassisinstanceof 方法来判断对象间是否存在我们期望的关系.

元类 metaclass

<class 'type'> 是python中一种特殊的class对象, 这种特殊的class对象称为 metaclass 对象, 元类就是用来创建类的东西. 还有一种特殊的对象 <class 'object'> , Python中任何一个class都必须直接或间接继承object.

例子中的 A 其实即是class对象, 也是instance 对象. 说是class对象, 因为它能通过实例化动作创建对象, 说它是instance对象, 因为它是metaclass的一个实例.大多数情况下metaclass都是 <class 'type'> 而在python内部, 它是PyType_Type. python中任何class对象都直接或间接继承 <class 'object'> ,对应python内部的PyBaseObject_Type .

从type对象到class对象

type实际上是一个元类。type就是Python在背后用来创建所有类的元类。现在你想知道那为什么type会全部采用小写形式而不是Type呢?好吧,我猜这是为了和str保持一致性,str是用来创建字符串对象的类,而int是用来创建整数对象的类。type就是创建类对象的类。

类的概念是class对象, class对象其实也是一个PyObject结构体. 在python启动时, 会对类型系统(对象模型)进行初始化的动作. 这个初始化的动作会动态在内置类型的PyTypeObject中设置一些重要的东西, 如tp_dict. 从而完成内置type类型从type对象到class对象的转变. 这个初始化动作从 _Py_ReadyTypes 开始的:

[object.c]
void _Py_ReadyTypes(void)
{
    if (PyType_Ready(&PyBaseObject_Type) < 0)
        Py_FatalError("Can't initialize object type");

    if (PyType_Ready(&PyType_Type) < 0)
        Py_FatalError("Can't initialize type type");

    if (PyType_Ready(&_PyWeakref_RefType) < 0)
        Py_FatalError("Can't initialize weakref type");

    if (PyType_Ready(&_PyWeakref_CallableProxyType) < 0)
        Py_FatalError("Can't initialize callable weakref proxy type");

    ... // 逐个 PyType_Ready 所有内置类型对象
}

_Py_ReadyTypes 中, 会调用 PyType_Ready 对class对象进行初始化, 类型对象在python启动后已经作为全局变量的存在了, 需要的仅仅是完善. PyType_Type 是个比较特殊的class对象, 就从 PyType_Ready(&PyType_Type) 开始.

处理基类和type信息

[typeobject.c]
int PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;

    // 尝试获得type的tp_base中指定的基类(super type)
    base = type->tp_base;
    if (base == NULL && type != &PyBaseObject_Type) {
        base = type->tp_base = &PyBaseObject_Type;
        Py_INCREF(base);
    }

    // 如果基类没有初始化, 先初始化基类, 采用递归调用PyType_Ready
    if (base != NULL && base->tp_dict == NULL) {
        if (PyType_Ready(base) < 0)
            goto error;
    }

    // 如果没有设置ob_type, 则使用基类的ob_type
    if (Py_TYPE(type) == NULL && base != NULL)
        Py_TYPE(type) = Py_TYPE(base);

    ...

}

PyType_Ready 中会获取待初始化的基类 tp_base , 如果没有设置则会设为为 &PyBaseObject_Type , 这就表明游离的类型对象的基类就成了 <class 'object'> 了, 内置类型对象在声明的时候的基类信息:

PyTypeObject tp_base
PyType_Type NULL
PyLong_Type NULL
PyBool_Type &PyLong_Type

这也对应了万物是对象的概念, 所有class对象都是直接或间接的以 <class 'object'> 为基类. 获得基类了, 判断基类是否已经被初始化了, 如果没有, 则先对基类进行初始化, 这个判断是根据 tp_dict 判断的, 也就是说这个初始化过程是对 tp_dict 进行填充的. tp_dict 是相当重要的, 对于如__add__ 的操作都是映射到这个字典的, 这个下文会说.

ob_type 对应了对象的 __class__ 的返回信息. 而 __bases__ 返回的是基类列表.

而在 PyBaseObject_Type 的定义中它的ob_type 设置为 PyType_Type , 所以呢... 是不是很乱. 像一个圈又绕回去了, 其实这里是两条线索, PyBaseObject_Type 是继承链的顶端, 而 PyType_Type 是类型链的顶端.

基类 PyBaseObject_Type 的初始化

既然是先初始化基类, 那么我们得先分析 PyType_Ready(&PyBaseObject_Type) , 如果是基类,显然上面代码中三个if全是false. 就是什么动作也没有发生, 继续看看后续的代码.

处理基类列表

Python虚拟机处理类型初始化, 因为python支持多继承, 所以每一个class都有一个基类列表:

[typeobject.c]
int PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;

    base = type->tp_base;
    ... // 三个if

    // 处理bases, 基类列表
    bases = type->tp_bases;
    if (bases == NULL) {
        if (base == NULL)
            bases = PyTuple_New(0); // 如果base为空, 说明是PyBaseObject_Type, 设置空tuple
        else
            bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            goto error;
        type->tp_bases = bases;
    }

    ...

}

对于 PyBaseObject_Type 来说, 它的tp_base和tp_bases都是NULL, 所以它的基类列表是一个空tuple. 而入PyLong_Type来说, 虽然tp_bases为空, 但是base不为空, 因此他们的基类列表至少包含一个 PyBaseObject_Type.

填充tp_dict

填充 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;
    }
    ...    
}    

在这个阶段, 将 ('__add__', &nb_add) 加入到 tp_dict . 这个阶段是调用 add_operators, add_methods, add_members, tp_getset 函数实现的. 那么,初始化过程中是如何知道 add 和nb_add是有关联的呢? 这个关联是在python中预先就确定了, 存放在一个名为 slotdefs 的全局数组中.

slot与操作排序

slot可以视为是表示PyTypeObject 中定义的操作, 在一个操作对应一个slot:

[typeobject.c]
typedef struct wrapperbase slotdef;

[descobject.h]
struct wrapperbase {
    const char *name;
    int offset;
    void *function;
    wrapperfunc wrapper;
    const char *doc;
    int flags;
    PyObject *name_strobj;
};

一个slot中, 存储这与PyTypeObject中一种操作对应的各种信息.成员 name 表示操作应该的名称, 如 "__add__" ; offset 则表示操作的函数在PyHeapTypeObject中的偏移量; function 则指向一个称谓slot function的函数.

为了定义一个slot, Python提供了多个宏来定义, 其中最基本的有两个:

[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)}

这两个宏的区别就是 TPSLOT 计算的是函数指针在PyTypeObject中的偏移量; 而 ETSLOT 计算的是函数指针PyHeapTypeObject中的偏移量.

[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, 所以TPSLOT计算的偏移量同时也是PyHeapTypeObject的偏移量. 对于PyTypeObject来说, 有一些操作如 nb_add 其函数指针是PyNumberMethods结构的 tp_as_number 中存放改的, 因此这种情况TPSLOT就不能用了.

那要用什么方法来获取, 这个比较难理解, 这个offset是用来对操作进行排序的:

[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 SQSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
    ETSLOT(NAME, as_sequence.SLOT, FUNCTION, WRAPPER, DOC)

static slotdef slotdefs[] = {
    ...
    // 不同操作名对象相同操作
    BINSLOT("__add__", nb_add, slot_nb_add, "+"),
    RBINSLOT("__radd__", nb_add, slot_nb_add, "+"),
    BINSLOT("__sub__", nb_subtract, slot_nb_subtract, "-"),
    RBINSLOT("__rsub__", nb_subtract, slot_nb_subtract, "-"),

    // 相同操作名对应不同操作
    MPSLOT("__getitem__", mp_subscript, slot_mp_subscript, wrap_binaryfunc, "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
    SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item, "__getitem__($self, key, /)\n--\n\nReturn self[key]."),
    ...
}

其中 BINSLOTSQSLOT 是对ETSLOT的一个简单包装, 可以发现, 操作名(如add) 和操作并不是一一对应的, 存在同一个操作名对应不同操情况, 也存在不同操作名对应同一个操作.

相同操作名不同操作. 在进行tp_dict填充时, 就会出现一个问题, "__getitem__" 对应在tp_dict中是 sq_item 还是 mp_subscript 呢? 为了解决这个问题, 就需要利用slot中的offset信息对操作进行排序. 在PyHeapTypeObject的定义中,PyMappingMethods(里面有mp_subscript)在 PySequenceMethods(里面有sq_item)的前面, 因此就有偏移的大小关系: offset(mp_subscript) < offset(sq_item) . 如果一个PyTypeObject对象里有mp_subscript也有sq_item. 那么虚拟机会将mp_subscript于getitem绑定.

对slotdefs的排序在 init_slotdefs 中完成:

[typeobject.c]
static void init_slotdefs(void)
{
    slotdef *p;

    if (slotdefs_initialized)   // 初始只进行一次
        return;
    for (p = slotdefs; p->name; p++) {
        /* Slots must be ordered by their offset in the PyHeapTypeObject. */
        assert(!p[1].name || p->offset <= p[1].offset);
        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;
}

slot的描述信息descriptor

既然操作名一样会有不同的操作, 那么就需要对操作添加个说明信息加以区分. 这个就是成为descriptor的东西. 结构是 PyWrapperDescrObject :

[descrobject.h]
typedef struct {
    PyDescr_COMMON;
    struct wrapperbase *d_base;
    void *d_wrapped; /* This can be any function pointer */
} PyWrapperDescrObject;

#define PyDescr_COMMON PyDescrObject d_common
typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

它的创建通过 PyDescr_NewWrapper :

[descrobject.c]
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;
}

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 = PyUnicode_InternFromString(name);
        if (descr->d_name == NULL) {
            Py_DECREF(descr);
            descr = NULL;
        }
        else {
            descr->d_qualname = NULL;
        }
    }
    return descr;
}

python内部各种dscriptor都包含 PyDescr_COMMON , 其中的 d_type 被设置为 PyWrapperDescr_Type , 而 d_wrapped 则返回这重要的信息: 操作对应的函数指针.

举个栗子, 比如 PyList_Type 来说, 它的 tp_dict["__getitem__"].d_wrapped 就是 &mp_subscript. 而slot则被存放在 d_base 中.

建立关系

slotdefs 中需要为每一个slot建立一个descriptor, 然后在tp_dict中建立操作名到descriptor的关联. 为slot建立descriptor是在 add_operators 中完成的:

[typeobject.c]
static int add_operators(PyTypeObject *type)
{
    PyObject *dict = type->tp_dict;
    slotdef *p;
    PyObject *descr;
    void **ptr;

    init_slotdefs();    // slotdefs初始化
    for (p = slotdefs; p->name; p++) {
        if (p->wrapper == NULL) // 如果slot中没有指定wrapper, 跳过
            continue;
        ptr = slotptr(type, p->offset);                 // 获得slot对应的操作在PyTypeObject中的指针
        if (PyDict_GetItem(dict, p->name_strobj))       // 如果tp_dict已经存在操作名, 跳过
            continue;

        descr = PyDescr_NewWrapper(type, p, *ptr);      // 为slot创建descriptor
        PyDict_SetItem(dict, p->name_strobj, descr);    // 将(操作名, descriptor)作为键值对放入tp_dict中.

    }
    return 0;
}

add_operators 函数中, 通过 slotptr 函数获得该slot对应的操作在PyTypeObject中的函数指针, 并借由这个函数指针创建descriptor, descriptor是对此函数指针的一个封装. 以 (p->name_strobj, descr) 作为key,valuse的形式填充到 tp_dict 中.

因为在创建desc之前, 会判断tp_dict中是否已经填充了操作名, 也就是对于相同操作名来说, 序号排在前面的操作才是能被填充的.

操作重写

考虑如下代码:

>>> class A(list):
...     def __repr__(seft):
...         return 'python'
... 
>>> a = A()
>>> a
python

__repr__ 是python的用来将一个对象以字符串展示的一个函数, '%s' % a 时, 会最终调用 A.__repr__ . 这里显然最终调用的是我们自定义的 __repr__ . 这意味着python在初始化A时, 对 tp_repr 进行了特殊处理.

在slotdefs中有一条slot为 TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc, "__repr__($self, /)\n--\n\nReturn repr(self).") , 在Python虚拟机初始化 class A时, 会先检查是否存在用户自定义的 __repr__ 函数, 如果有, 虚拟机从slot中沿着搜索顺序找到 tp_repr , 并将函数指针替换为 &slot_tp_repr , 因此, 执行A.tp_repr时实际执行的是 slot_tp_repr :

[typeobject.c]
static PyObject * slot_tp_repr(PyObject *self)
{
    PyObject *func, *res;
    _Py_IDENTIFIER(__repr__);
    int unbound;

    // 查找__repr__属性
    func = lookup_maybe_method(self, &PyId___repr__, &unbound);
    if (func != NULL) {
        // 调用对应的 __repr__ 定义的方法
        res = call_unbound_noarg(unbound, func, self);
        Py_DECREF(func);
        return res;
    }
    PyErr_Clear();
    return PyUnicode_FromFormat("<%s object at %p>",
                               Py_TYPE(self)->tp_name, self);
}

slot_tp_repr 中, 会寻找 __repr__ 属性对应的函数, 如果有则执行, 这就完成了自定义函数替换list的默认repr行为. 当然这个方法repr覆盖只是对于class A. PyList_Type中的repr并没有改变.

对于A来说, 这个变化是在 fixup_slot_dispatchers(PyTypeObject *type) 中完成, 对于内置class对象, 不会有这个操作, 这层操作是属于用户自定义class的.

static void fixup_slot_dispatchers(PyTypeObject *type)
{
    slotdef *p;

    init_slotdefs();
    for (p = slotdefs; p->name; )
        p = update_one_slot(type, p);
}

这部分会对slotdefs中每个slot进行更新.

确定MRO

MRO 全称 Method Resolution Order,它代表了类继承的顺序。也是一个一个class对象的属性解析顺序. 因为python支持多继承, 在多重继承中, 就必须设置按照某个顺序来解析属性. 而例如java, php只支持单继承就没这个问题了.

考虑如下代码:

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

可以看到在D的基类A和B中都实现了show方法, 那么在调用 d.show() 时, 就需要给出由哪个来执行. pythoin内部中在 PyType_Ready 中通过 mro_internal 函数完成对一个类型的 mro 顺序的建立. 这是一个tuple类型的对象, 依次放入class对象, 这个顺序就是mro的顺序, 最终这个tuple保存在 PyTypeObject.tp_mro 中.

简单的表示下D的继承关系:

D -> C -> B
     |    |
     A    list
     |    |
    list object
     |
    object

mro列表的建立过程

这个跟踪过程是:

  1. 首先获得D, D的mro列表tp_mro中没有D, 所以放入D
  2. 获得C, 发现D的mro列表没有C, 放入C. 发现C中存在mro列表. 访问mro列表
    • 获得A. D的mro列表没有A, 放入A
    • 获得list, 跳过, 此时, 尽管D的mro中没有list, 但B的mro有, 这边的list添加会推迟处理.
    • 获得object, 同样推迟处理
  3. 获得B, 发现D的mro列表没有B, 放入B. 访问D的mro列表.
    • 获得list, 向D的mro列表放入list
    • 获得object, 向D的mro列表放入object

处理后的D的mro列表就是:

(<class '__main__.D'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'list'>, <class 'object'>)

继承基类操作

是不是比较好奇推迟处理是怎么办到的. 在设置mro列表时, 第一项总是其自身. mro列表的每个元素中, 都直接或间接继承自object类, 也就是说, 他们总有一些公共继承的部分. 因此需要计算他们的继承顺序.这个由 mro_internal 完成, 为了理解设置顺序, 以传入PyBool_Type来分析.

[typeobject.c]
int PyType_Ready(PyTypeObject *type)
{
    ...
    /* Calculate method resolution order */
    if (mro_internal(type, NULL) < 0)
        goto error;
    ...
}

static int mro_internal(PyTypeObject *type, PyObject **p_old_mro)
{
    PyObject *new_mro, *old_mro;
    int reent;

    old_mro = type->tp_mro; // 初始的PyBool_Type.tp_mro 是null的
    Py_XINCREF(old_mro);
    // 可能会引起重入
    new_mro = mro_invoke(type);  /* might cause reentrance */
    reent = (type->tp_mro != old_mro);
    Py_XDECREF(old_mro);
    if (new_mro == NULL)
        return -1;

    if (reent) {
        Py_DECREF(new_mro);
        return 0;
    }

    type->tp_mro = new_mro;

    type_mro_modified(type, type->tp_mro);

    type_mro_modified(type, type->tp_bases);

    PyType_Modified(type);

    if (p_old_mro != NULL)
        *p_old_mro = old_mro;  /* transfer the ownership */
    else
        Py_XDECREF(old_mro);

    return 1;
}

显然是通过 new_mro = mro_invoke(type); 创建一个新的tuple. 将其 type->tp_mro = new_mro; 因此要看看mro_invoke函数完成了什么:

static PyObject *mro_invoke(PyTypeObject *type)
{
    PyObject *mro_result;
    PyObject *new_mro;
    int custom = (Py_TYPE(type) != &PyType_Type); // 判断是否是用户自定义的类, 这里PyBool_Type显然不是

    if (custom) { // 既然custom为0, 这里不贴了
        ...
    }
    else {
        mro_result = mro_implementation(type);
    }
    if (mro_result == NULL)
        return NULL;

    new_mro = PySequence_Tuple(mro_result); // 转成tuple类型

    return new_mro;
}

static PyObject * mro_implementation(PyTypeObject *type)
{
    PyObject *result = NULL;
    PyObject *bases;
    PyObject *to_merge, *bases_aslist;
    int res;
    Py_ssize_t i, n;

    bases = type->tp_bases;     // 获取直接父类
    n = PyTuple_GET_SIZE(bases);

    to_merge = PyList_New(n+1);
    if (to_merge == NULL)
        return NULL;

    // 遍历直接父类,将其mor添加到to_merge中   to_merge = [x.__mro__ for x in type.__bases__]
    for (i = 0; i < n; i++) {
        PyTypeObject *base;
        PyObject *base_mro_aslist;
        base = (PyTypeObject *)PyTuple_GET_ITEM(bases, i);
        base_mro_aslist = PySequence_List(base->tp_mro);    // 转成list对象
        PyList_SET_ITEM(to_merge, i, base_mro_aslist);      // 父类的mro拷贝到to_merge
    }

    bases_aslist = PySequence_List(bases);

    PyList_SET_ITEM(to_merge, n, bases_aslist);     // to_merge.append(type.__bases__)

    result = Py_BuildValue("[O]", (PyObject *)type);// result = [type, ] 自身类型, 也就是最终mro的第一元素
    if (result == NULL)
        goto out;

    res = pmerge(result, to_merge); // 合并
    if (res < 0)
        Py_CLEAR(result);

  out:
    Py_DECREF(to_merge);

    return result;
}

这个过程是这样的, 在mro_implementation 收集父类的mro和直接父类作为一个列表. 再创建一个只有自身元素的列表, 对这两个列表进行合并. 还是用例子还说明比较合适. 以最上面的A B C D类的来, 我们来计算class D的mro. 这里有个前提就是, 在设置D的mro时, 其父类的mro都是确定的, 有点像动态规划有木有. 因此我们能获得的信息有:

A.__mro__ = (<class '__main__.A'>, <class 'list'>, <class 'object'>)
B.__mro__ = (<class '__main__.B'>, <class 'list'>, <class 'object'>)
C.__mro__ = (<class '__main__.C'>, <class '__main__.A'>, <class 'list'>, <class 'object'>)

D.__bases__ = (<class '__main__.C'>, <class '__main__.B'>)

构建一个待合并的数组, 它就是 to_merge = [x.__mro__ for x in D.__bases__]; to_merge.append(D.__bases__) 和 一个只有D的数组 result = [D, ] :

to_merge = [
    (<class '__main__.C'>, <class '__main__.A'>, <class 'list'>, <class 'object'>), 
    (<class '__main__.B'>, <class 'list'>, <class 'object'>), 
    (<class '__main__.C'>, <class '__main__.B'>)
]

result = [<class '__main__.D'>, ]

合并的过程由 pmerge(PyObject *acc, PyObject* to_merge) 函数完成, 主要判断待加的基类如果在后续列表中存在, 则跳过. 也就是推迟添加.

有一个不理解的地方是, 我觉得构建 to_merge 时, to_merge.append(type.__bases__) 是多余的, 去掉后也不影响后续的合并, 不清楚出于什么考虑源码中有加.

子类继承基类中定义的方法

python虚拟机会将基类定义的方法拷贝到子类中, 从而完成对基类操作的继承动作. 这个继承的动作由 inherit_slots 完成:

[typeobject.c]
int PyType_Ready(PyTypeObject *type)
{
    ...
    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);
        COPYNUM(nb_subtract);
        COPYNUM(nb_multiply);
        COPYNUM(nb_remainder);
        ...
    }
    ...
}

我们知道 PyBool_Type 中没有设置 nb_add 的操作的, 但它的父类 tp_base 设置是 PyLong_Type , 而 PyLong_Type 中定义 nb_add 操作. 这样就将父类的操作复制过来.

填充基类的子类列表

PyType_Ready 中还剩一个动作了, 就是设置基类中的子列表. 在 PyTypeObject 中定义了一个 tp_subclasses , 它是一个dict对象, 保存这所有继承自该类型的class对象.

[typeobject.c]
int PyType_Ready(PyTypeObject *type)
{
    ...
    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;
    }
    ...
}

填充子类由 add_subclass(PyTypeObject *base, PyTypeObject *type) 完成的, 是向直接父类中填充, 填充的结果我可以用如 int.__subclasses__() 来获取到:

[typeobject.c]
static int add_subclass(PyTypeObject *base, PyTypeObject *type)
{
    int result = -1;
    PyObject *dict, *key, *newobj;

    dict = base->tp_subclasses;
    if (dict == NULL) { // 如果为空, 创建一个新的dict
        base->tp_subclasses = dict = PyDict_New();
    }
    assert(PyDict_CheckExact(dict));
    key = PyLong_FromVoidPtr((void *) type);    // 类型在内存中的地址作为key
    if (key == NULL)
        return -1;
    newobj = PyWeakref_NewRef((PyObject *)type, NULL); // 将type转换为弱引用
    if (newobj != NULL) {
        result = PyDict_SetItem(dict, key, newobj);
        Py_DECREF(newobj);
    }
    Py_DECREF(key);
    return result;
}

总结下 PyType_Ready 的动作:

  • 设置type信息和基类信息
  • 填充tp_dict
  • 确定mro列表
  • 子类继承父类的操作
  • 设置基类的子类列表tp_subclasses

有时候在处理不同类型的时候, 有的操作不一定完全相等. 有的类型可能继承比较多, 有的可能比较少, 有时候要自己跟踪 PyType_Ready 才能了解各个细节.


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧