楔子
在前面的文章中我们说到,面向对象理论中的类和对象这两个概念在 Python 内部都是通过对象实现的。类是一种对象,称为类型对象,类实例化得到的也是对象,称为实例对象。但是对象在 Python 的底层是如何实现的呢?Python 解释器是基于 C 语言编写的 ,但 C 并不是一个面向对象的语言,那么它是如何实现 Python 的面向对象的呢?
首先对于人的思维来说,对象是一个比较形象的概念,但对于计算机来说,对象却是一个抽象的概念。它并不能理解这是一个整数,那是一个字符串,计算机所知道的一切都是字节。通常的说法是:对象是数据以及基于这些数据所能进行的操作的集合。在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也可能是离散的。
而 Python 的任何对象在 C 中都对应一个结构体实例,在 Python 中创建一个对象,等价于在 C 中创建一个结构体实例。所以 Python 的对象,其本质就是 C 的 malloc 函数为结构体实例在堆区申请的一块内存。
下面我们就来分析一下对象在 C 中是如何实现的。
对象的地基:PyObject
Python 一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息位于 PyObject 中,它是 Python 对象机制的核心,下面来看看它的定义。
// Include/object.h
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
首先解释一下结构体里面的 _PyObject_HEAD_EXTRA,这是一个宏,定义如下。
// Include/object.h
// 如果定义了宏 Py_TRACE_REFS
#ifdef Py_TRACE_REFS
// 那么 _PyObject_HEAD_EXTRA 会展开成如下两个字段
// 显然程序中创建的对象会组成一个双向链表
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
// 用于将 _ob_next 和 _ob_prev 初始化为空
#define _PyObject_EXTRA_INIT 0, 0,
// 否则说明没有定义宏 Py_TRACE_REFS
// 那么 _PyObject_HEAD_EXTRA 和 _PyObject_EXTRA_INIT 不会有任何作用
#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif
所以如果定义了宏 Py_TRACE_REFS,那么展开之后 PyObject 就是下面这样。
typedef struct _object {
PyObject *_ob_next;
PyObject *_ob_prev;
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
但 Py_TRACE_REFS 一般只在编译调试的时候会开启,我们从官网下载的都是 Release 版本,不包含这个宏,因此这里我们也不考虑它。所以 PyObject 最终就等价于下面这个样子:
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
所以 PyObject 里面包含了两个字段,分别是 ob_refcnt 和 ob_type。
ob_refcnt 表示对象的引用计数,当对象被引用时,ob_refcnt 会自增 1;引用解除时,ob_refcnt 会自减 1。而当对象的引用计数为 0 时,则会被回收。
那么在哪些情况下,引用计数会加 1 呢?哪些情况下,引用计数会减 1 呢?
引用计数加 1 的情况:
- 对象被创建:比如 name = "古明地觉",此时对象就是 "古明地觉" 这个字符串,创建成功时它的引用计数为 1;
- 变量传递使得对象被新的变量引用:比如 name2 = name;
- 引用该对象的某个变量作为参数传递到一个函数或者类中:比如 func(name);
- 引用该对象的某个变量作为元组、列表、集合等容器的元素:比如 lst = [name];
引用计数减 1 的情况:
- 引用该对象的变量被显式地销毁:del name;
- 引用该对象的变量指向了别的对象:name = "";
- 引用该对象的变量离开了它的作用域,比如函数的局部变量在函数执行完毕时会被删除;
- 引用该对象的变量所在的容器被销毁,或者变量从容器里面被删除;
因为变量只是一个和对象绑定的符号,更接地气一点的说法就是,变量是个便利贴,贴在指定的对象上面。所以 del 变量 并不是删除变量指向的对象,而是删除变量本身,可以理解为将对象身上的便利贴给撕掉了,其结果就是对象的引用计数减一。至于对象是否被删除(回收)则是解释器判断其引用计数是否为 0 决定的,为 0 就删,不为 0 就不删,就这么简单。
然后看一下字段 ob_refcnt 的类型,该类型为 Py_ssize_t,它是 ssize_t 的别名,在 64 位机器上等价于 int64。因此一个对象的引用计数不能超过 int64 所能表示的最大范围。但很明显,如果不费九牛二虎之力去写恶意代码,是不可能超过这个范围的。
说完了 ob_refcnt,再来看看 PyObject 的另一个字段 ob_type,相信你能猜到它的含义。对象是有类型的,类型对象描述实例对象的行为,而 ob_type 存储的便是对应的类型对象的指针,所以类型对象在底层是一个 struct _typeobject 结构体实例。另外 struct _typeobject 还有一个类型别名叫 PyTypeObject,关于类型对象,我们后续再聊。
以上就是 PyObject,它的定义非常简单,就一个引用计数和一个类型对象的指针。这两个字段的大小都是 8 字节,所以一个 PyObject 结构体实例的大小是 16 字节。由于 PyObject 是所有对象都具有的,换句话说就是所有对象对应的结构体都内嵌了 PyObject,因此你在 Python 里面看到的任何一个对象都有引用计数和类型这两个属性。
>>> num = 666
>>> sys.getrefcount(num)
2
>>> num.__class__
<class 'int'>
>>> sys.getrefcount(sys)
56
>>> sys.__class__
<class 'module'>
>>> sys.getrefcount(sys.path)
2
>>> sys.path.__class__
<class 'list'>
>>> def foo(): pass
...
>>> sys.getrefcount(foo)
2
>>> foo.__class__
<class 'function'>
引用计数可以通过 sys.getrefcount 函数查看,类型可以通过 type(obj) 或者 obj.__class__ 查看。
可变对象的地基:PyVarObject
PyObject 是所有对象的核心,它包含了所有对象都共有的信息,但是还有那么一个属性虽然不是每个对象都有,但至少有一大半的对象会有,能猜到是什么吗?
之前说过,对象根据所占的内存是否固定,可以分为定长对象和变长对象,而变长对象显然有一个长度的概念,比如字符串、列表、元组等等。即便是相同类型的实例对象,但是长度不同,所占的内存也是不同的。比如字符串内部有多少个字符,元组、列表内部有多少个元素,显然这里的多少也是 Python 中很多对象的共有特征。虽然不像引用计数和类型那样是每个对象都必有的,但也是绝大部分对象所具有的。
所以针对变长对象,Python 底层也提供了一个结构体,因为 Python 里面很多都是变长对象。
// Include/object.h
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size;
} PyVarObject;
我们看到 PyVarObject 实际上是 PyObject 的一个扩展,它在 PyObject 的基础上添加了一个 ob_size 字段,用于记录内部的元素个数。比如列表,列表的 ob_size 维护的就是列表的元素个数,插入一个元素,ob_size 会加 1,删除一个元素,ob_size 会减 1。
因此使用 len 函数获取列表的元素个数是一个时间复杂度为 O(1) 的操作,因为 ob_size 始终和内部的元素个数保持一致,所以会直接返回 ob_size。
所有的变长对象都拥有 PyVarObject,而所有的对象都拥有 PyObject,这就使得在 Python 中,对对象的引用变得非常统一。我们只需要一个 PyObject * 就可以引用任意一个对象,而不需要管这个对象实际是一个什么样的对象。
所以 Python 变量、以及容器内部的元素,本质上都是一个 PyObject *。而在操作变量的时候,也要先根据 ob_type 字段判断指向的对象的类型,然后再寻找该对象具有的方法,这也是 Python 效率慢的原因之一。
由于 PyObject 和 PyVarObject 要经常使用,所以底层提供了两个宏,方便定义。
// Include/object.h
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
比如定长对象浮点数,在底层对应的结构体为 PyFloatObject,它只需在 PyObject 的基础上再加一个 double 即可。
typedef struct {
// 等价于 PyObject ob_base;
PyObject_HEAD
double ob_fval;
} PyFloatObject;
再比如变长对象列表,在底层对应的结构体是 PyListObject,所以它需要在 PyVarObject 的基础上再加一个指向指针数组首元素的二级指针和一个容量。
typedef struct {
// 等价于 PyVarObject ob_base;
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
这上面的每一个字段都代表什么,我们之前提到过,当然这些内置的数据结构后续还会单独剖析。
对于 PyListObject,里面的 ob_item 就是指向指针数组首元素的二级指针,而 allocated 表示已经分配的容量,一旦添加元素的时候发现 ob_size 自增 1 之后会大于 allocated,那么解释器就知道数组已经满了(容量不够了)。于是会申请一个长度更大的指针数组,然后将旧数组内部的元素按照顺序逐个拷贝到新数组里面,并让 ob_item 指向新数组的首元素,这个过程就是列表的扩容,后续在剖析列表的时候还会细说。
所以我们看到列表在添加元素的时候,地址是不会改变的,即使容量不够了也没关系,直接让 ob_item 指向新的数组就好了,至于 PyListObject 对象(列表)本身的地址是不会变化的。
小结
PyObject 是 Python 对象的核心,因为 Python 对象在 C 的层面就是一个结构体,并且所有的结构体都嵌套了 PyObject 结构体。而 PyObject 内部有引用计数和类型这两个字段,因此我们可以肯定的说 Python 的任何一个对象都有引用计数和类型这两个属性。
另外大部分对象都有长度的概念,所以又引入了 PyVarObject,它在 PyObject 的基础上添加了一个 ob_size 字段,用于描述对象的长度。比如字符串内部的 ob_size 维护的是字符串的字符个数,元组、列表、集合等等,其内部的 ob_size 维护的是存储的元素个数,所以使用 len 函数获取对象长度是一个 O(1) 的操作。
欢迎大家关注我的公众号:古明地觉的编程教室。
如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。