《源码探秘 CPython》69. 给类型对象设置类型和基类信息
在上一篇文章中我们说道,内置类对象虽然在底层静态定义好了,但是还不够完善。解释器在启动之后还要再打磨一下,然后才能得到我们平时使用的类型对象,而这个过程被称为类型对象的初始化。
类型对象的初始化,是通过 PyType_Ready 函数实现的,我们来看一下,它位于 Objects/typeobject.c 中。另外由于初始化这部分内容比较多,接下来我们准备用三篇文章去介绍它。
int
PyType_Ready(PyTypeObject *type)
{
//这里的参数显然是类型对象
//以 <class 'type'> 为例
//dict:属性字典
//bases:继承的所有基类,即 __bases__
PyObject *dict, *bases;
//base:继承的第一个基类,即 __base__
PyTypeObject *base;
Py_ssize_t i, n;
//......
//......
//获取类型对象中 tp_base 成员指定的基类
base = type->tp_base;
if (base == NULL && type != &PyBaseObject_Type) {
//如果基类为空、并且该类本身不是object
//那么将该类的基类设置为 object、即 &PyBaseObject_Type
//所以一些类型对象在底层定义的时候,tp_base 成员为空
//因为tp_base是在这里、也就是初始化的时候进行设置的
base = type->tp_base = &PyBaseObject_Type;
Py_INCREF(base);
}
//如果基类不是NULL,也就是指定了基类
//但是基类的属性字典是NULL
if (base != NULL && base->tp_dict == NULL) {
//说明该类的基类尚未初始化,那么会先对基类进行初始化
//注意这里的 tp_dict,它表示每个类都会有的属性字典
//而属性字典是否为 NULL,是类型对象是否初始化完成的重要标志
if (PyType_Ready(base) < 0)
goto error;
}
//如果该类型对象的 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 也为空,那么 bases 就是空元祖
//而base如果为空了,说明当前的类对象一定是object
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,这个ob_type就是 __class__ 返回的类型对象。
首先 PyType_Ready 函数里面接收的是一个 PyTypeObject 对象,我们知道这个在 Python 中就是类对象。因此这里是设置这些类对象的 ob_type,那么对应的显然就是元类(metaclass),我们自然会想象到Python的type。
而Py_TYPE(type) = Py_TYPE(base)这一行代码是把父类的ob_type设置成了当前类的ob_type,那么这一步的意义何在呢?我们使用Python来演示一下。
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。
if (Py_TYPE(type) == NULL && base != NULL)
Py_TYPE(type) = Py_TYPE(base);
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。对于 B 而言也是一样的。
然后我们看看 C,因为 C 没有显式地继承任何类,那么 tp_bases 就是NULL。但是Python3里面所有的类都默认继承了object,所以tp_base就是object。而 tp_bases,显然是 (object,)。
以上就是 tp_base、ob_type、tp_bases 的设置,还是比较简单的,它们在设置完毕之后,就要对 tp_dict 进行填充了。而填充 tp_dict 是一个极其繁复的过程,我们下一篇文章再说。