cpython解释器源码剖析(二)——类型系统

下文中的Python指的是在官方cpython解释器上实现的Python 3.8

1. 一切皆对象

Python之中一切皆对象,更具体些说一切都是指向对象的指针(虽然Python本身并无指针的概念)。

这一点Python远比Java更为激进,Java虽然也有“一切皆对象”的口号,但是至少intdouble等基础类型就不是以对象的形式存在,且对象的方法作为对象的子组件自然也不是对象。但是在Python中则连最基本的整数值是以对象,对象中的方法本身也是另一个对象。可以说是名副其实地做到了一切皆是对象。

2. 连类型也是对象

C++明确区分了类型和对象,前者是一个理念中存在的形式,后者是绑定了特定存储空间的一个内容。

Java中对类型虽然也是都是java.lang.Class类的对象,但是除了反射等少数使用场景,类型和对象之间通常也是泾渭分明的。

而到了Python中,二者却已经到了难分难解的地步了。类型可以像普通对象一样建立、修改、销毁,唯一不同的只是可以产生对象。因此Python中将能产生对象的对象(亦即类型)称为类型对象(定语类型表示其特征,中心语对象表示一切皆对象的本质),其他的对象(寻常所言对象)称为实例对象。

类型对象和实例对象之间,存在着实例化关系,对应者Python内建函数isinstance,例如isinstance(1, int)的返回值是Trueisinstance(1.23, int)的返回值是False。同时,任何对象都有且只有唯一的类型对象,可以通过对象的__class__属性获取,即isinstance(x, x.__class__)恒为真,其中特殊的是,类型对象的类型是type(称为元类型对象),而type的类型确为其本身。

类型对象和类型对象之间,存在着继承关系,对应着Python内建函数issubclass,例如issubclass(bool, int)的返回值为Trueissubclass(bool, float)的返回值为False,而普通实例对象不能参与issubclass判断。任何类型的父类是一个tuple,可以通过类型对象的__mro__(Method Resolution Order)属性获取,如bool的属性值为(bool, int, object),这表示了实现多态性的搜索顺序,从最左开始,如果失败则尝试在父类这种搜索,显然这个列表总会从类型自身开始而终于object

举一个例子,

class A:
pass

a = A()
i = 1234
graph LR
type(type) -.-> type
type -.-> object(object)
type -.-> A(A)
type -.-> int(int)
A -.-> a((a))
int -.-> i((i))
object --> type
object --> A
object --> int

图中方形为类型对象,圆形为实例对象,虚线为类型实例化出对象,实线为类型派生出子类。

其中最难理解的其实是两个回环:

  • 其一是类型对象type的类型为自身
  • 其二是type实例化了object,而前者却反而是object的子类

这两个环的存在像是一个逻辑上的悖论,就如同先有鸡还是先有蛋的问题。解开这种悖论途径便是来自于系统之外的“第一推动力”,就如同原始动物进化出了第一只会下蛋的动物(称为鸡),便可以无蛋生鸡。Python的第一推动力便是解释器背后的C语言代码,它在Python执行环境还为成形之时便将typeobject这两个类型对象统统构造好并设置其相对关系。

3. 解释器源码中的Python对象

Python中的对象在解释器中其实都是一段内存数据,只不过这一段数据有两副视图,一副是在解释器C源码中数据结构实例视图,一副是在Python运行环境中的所呈现的Python对象视图。

首先是所有类的基础object类,他在解释器中的数据视图定义如下(已去除调试信息):

// Include/object.h
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;

其中ob_refcnt是一个引用计数值,用以GC。而struct _typeobject *是一个指向类型对象的指针,所以说Python是鸭子类型但不是无类型,是动态类型但是具有明确类型,因为每个Python对象有有且只有唯一的类型,通过对象中的指针字段实现了从实例对象到对应类型对象的唯一绑定。

由于C语言不像C++一样支持类和数据结构的继承,所以其中数据结构的继承方式通过在子类数据结构头部申明一个父类对象来实现:

// Include/object.h
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

// Include/cpython/tupleobject.h
typedef struct {
PyVarObject ob_base;
/* ob_item contains space for 'ob_size' elements.
Items must normally not be NULL, except during construction when
the tuple is not yet visible outside the function that builds it. */
PyObject *ob_item[1];
} PyTupleObject;

// Include/listobject.h
typedef struct {
PyVarObject ob_base;
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;

/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject;

PyVarObject定义的是一个带变长内容的对象数据结构,其中在头部声明了一个PyObject字段,这样任意PyVarObject *指针也能当作PyObject *指针使用,之后才是其子类数据内容,这里是一个表明变长数据对象中元素个数的整数值。

然后类似地,从C语言视图说,PyTupleObject“继承”了PyVarObject,而在Python视图中来说就是tuple继承了一个未名的变长对象基类(实际上并未有那个Python类完全对应PyVarObject)。PyTupleObject尾部的指针数组(真实申请的长度不一定为1)则存储了tuple中所有元素的对象指针。

同理,PyListObject对应于Python环境中的list。但不同的是,由于tuple是不可变对象,因此其中的元素指针以指针数组的形式存在于数据结构尾部,只需要在malloc根据元素的数量皆可以确定实际申请内存块的大小;而list是可变对象,其中存储的元素可以在后期动态增删,创建时期无法知道所需要的内存空间大小,因此就在其中预留一个二级指针,指向实际存储元素指针的数组,另外allocated字段表示该数组预申请的大小,避免列表每增长一个元素就要去重新申请更大的内存空间和迁移数据。

前面提到Python中连类型都是对象(只不过特殊地,叫类型对象),那么其对应的结构体呢?

// Include/cpython/object.h
typedef struct _typeobject {
PyVarObject ob_base;
const char *tp_name;
// ... 省略几十行
} PyTypeObject;

// Objects/typeobject.c
PyTypeObject PyType_Type = {
{
{
1, // 引用计数
&PyType_Type // 类型对象指针
},
0 // 变长对象元素数
},
"type", // 名称
// ... 省略
};

// Objects/tupleobject.c
PyTypeObject PyTuple_Type = {
{
{
1,
&PyType_Type
},
0
},
"tuple",
// ... 省略
};
  • 首先,类型对象的数据结构PyTypeObject表示其是一个变长对象,因为其中的属性数量各部相同
  • 即使是对象类型也是类型,也要继承于PyObject(或者说Python视图中的object),PyType_Type中这个指针指向自己,在Python的角度说就是type类型对象的类型是它自己,同理,其他类型的类型是type。(这解释了前面提到的关于typeobject的两个回环和鸡生蛋问题)
  • 然后,每个类型都应该具有其在Python环境中的表达名字tp_name,就是其在Python解释器中表达的类名

4. 自定义类的对象

Python的内置基本类型,如inttuplelist等,其对应类型对象有对应的C语言数据结构定义(变量名PyXxx_Type),其实例对象都有对应的数据结构声明(结构体名PyXxxObject),这又两方面的考量:其一Python无法自举,必须先通过C语言构建基础;其二就是效率考量了,譬如有了list之后其实可以用纯Python的方式实现dict了,但是为了效率还是将之设计为内置类型用C语言实现。

而对于自定义对象:

class A:
pass

a = A()

当执行了class语句之后,Python会根据class中定义的方法和字段动态构建一个PyTypeObject实例,也就是一个类型对象,并赋给变量名A,于是A指向了一个(自定义的)类型对象。

当执行a = A()时,则会动态构建一个PyObject的子类对象,其中的类型指针指向了类型A,并具有一个字典对象字段,其中以<名称字符串-对象指针>的形式存储了Python对象的属性,即Python对象的__dict__属性。