楔子 在前面的文章中我们说到,面向对象理论中的类和对象这两个概念在 Python 内部都是通过对象实现的。类是一种对象,称为类型对象,类实例化得到的也是对象,称为实例对象。 但是对象在 Python 的底层是如何实现的呢?Python 解释器是基于 C 语言实现的 ,但 C 并不是一个面向对象的语言,那么它是如何实现 Python 的面向对象的呢? 首先对于人的思维来说,对象是一个比较形象的概念,但对于计算机来说,对象却是一个抽象的概念。它并不能理解这是一个整数,那是一个字符串,计算机所知道的一切都是字节。 通常的说法是:对象是数据以及基于这些数据所能进行的操作的集合。在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也可能是离散的。 而 Python 的任何对象在 C 中都对应一个结构体实例,在 Python 中创建一个对象,等价于在 C 中创建一个结构体实例。所以 Python 的对象,其本质就是 C 的 malloc 函数为结构体实例在堆区申请的一块内存。 下面我们就来分析一下对象在 C 中是如何实现的。 对象的地基:PyObject Python 一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息位于 PyObject 中,它是 Python 对象机制的核心,下面来看看它的定义。
我们看到具体定义位于 struct _object 中,PyObject 只是它的别名。
注:源码中定义的 struct _object 看起来会更复杂一些,因为里面还包含了一些宏判断,用于适配不同的操作系统和编译器。 这些宏判断我们不需要关注,对于当前的 64 位机器来说,等价于如下。
然后是 _PyObject_HEAD_EXTRA,它也是一个宏,定义如下。
关于 PyObject 的定义,再画一张图总结一下。 Py_TRACE_REFS 一般只在编译调试的时候会开启,我们从官网下载的都是 Release 版本,不包含这个宏,因此这里我们也不考虑它。 所以 PyObject 最终就等价于下面这个样子:
当然这两者也可以写在一起,即定义结构体的同时起一个别名。
方式是等价的,只不过 Python 将两者分开了,并写在了不同的文件中。 了解了 PyObject 的结构之后,我们再来看一下它内部的字段。 ob_refcnt:引用计数 ob_refcnt 表示对象的引用计数,当对象被引用时,ob_refcnt 会自增 1;引用解除时,ob_refcnt 会自减 1。而当对象的引用计数为 0 时,则会被回收。 那么在哪些情况下,引用计数会加 1 呢?哪些情况下,引用计数会减 1 呢? 导致引用计数加 1 的情况:
导致引用计数减 1 的情况:
因为变量只是一个和对象绑定的符号,接地气一点的说法就是变量是个便利贴,贴在指定的对象上面。所以 del 变量 并不是删除变量指向的对象,而是删除变量本身,可以理解为将对象身上的便利贴给撕掉了,其结果就是对象的引用计数减一。 至于对象是否被删除(回收)则是解释器判断引用计数是否为 0 决定的,为 0 就删,不为 0 就不删,就这么简单。 然后需要强调的是,在 3.12 之前的 Python 源码中,PyObject 是这么定义的,以 3.8 为例。 在 3.12 之前,引用计数通过一个 ob_refcnt 字段来维护,字段类型为 Py_ssize_t,它是 ssize_t 的别名,在 64 位机器上等价于 int64。因此一个对象的引用计数不能超过 int64 所表示的最大范围。但很明显,如果不费九牛二虎之力去写恶意代码,是不可能超过这个范围的。 还是很好理解的,但从 3.12 开始,却搞了个共同体(union)出来,这是为啥呢?因为 Python 从 3.12 开始引入了一个概念叫永恒对象。顾名思义,永恒对象就是那些永远不会被回收的对象。
永恒对象的引用计数为 uint32 类型的最大值,即 2 的 32 次方减 1,像 None、-5 到 256 之间的小整数,都属于永恒对象。 共同体中的 ob_refcnt 字段的作用还和之前一样,依旧是负责维护对象的引用计数。 但 ob_refcnt_split 也会维护一份引用计数,它是 uint32 类型的数组,长度为 2,但只会用数组的一个元素来维护。如果发现对象的引用计数达到了 uint32 的最大值,那么会将对象判定为永恒对象,而永恒对象永远不会被回收。 所以 ob_refcnt_split 是针对永恒对象引入的,它是一个长度为 2 的 uint32 类型的数组,大小是 8 字节。而 ob_refcnt 是 Py_ssize_t 类型,等价于 int64,大小也是 8 字节。由于这两者组成的是共同体,所以整体大小依旧是 8 字节,因此 PyObject 结构体实例的大小和之前一样。 当然啦,虽然引用计数是由共同体来维护,但你把它当成普通的 Py_ssize_t 类型的字段来理解也是可以的。因为 3.12 之前只有一个 ob_refcnt,而 ob_refcnt_split 是针对永恒对象专门引入的。 ob_type:类型指针 对象是有类型的,类型对象描述实例对象的行为,而 ob_type 存储的便是对应类型对象的指针,所以类型对象在底层是一个 PyTypeObject 结构体实例。 从这里可以看出,所有的类型对象在底层都是由同一个结构体实例化得到的,因为 PyObject 是所有对象共有的,它们的 ob_type 指向的都是 PyTypeObject。
以上就是 PyObject,它的定义非常简单,就一个引用计数和一个类型对象的指针。这两个字段的大小都是 8 字节,所以一个 PyObject 结构体实例的大小是 16 字节。 另外,由于 PyObject 是所有对象都具有的,换句话说就是所有对象对应的结构体内部都内嵌了 PyObject,因此你在 Python 里面看到的任何一个对象都有引用计数和类型这两个属性。
引用计数可以通过 sys.getrefcount 函数查看,类型可以通过 type(obj) 或者 obj.__class__ 查看。 可变对象的地基:PyVarObject PyObject 是所有对象的核心,它包含了所有对象都共有的信息,但是还有那么一个属性虽然不是每个对象都有,但至少有一大半的对象会有,能猜到是什么吗? 之前说过,对象根据所占的内存是否固定,可以分为定长对象和变长对象,而变长对象显然有一个长度的概念,比如字符串、列表、元组等等。即便是相同类型的实例对象,但是长度不同,所占的内存也是不同的。 比如字符串内部有多少个字符,元组、列表内部有多少个元素,显然这里的多少也是 Python 中很多对象的共有特征。虽然不像引用计数和类型那样是每个对象都必有的,但也是绝大部分对象所具有的。 所以针对变长对象,Python 底层也提供了一个结构体,因为 Python 里面很多都是变长对象。
我们看到 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 要经常被使用,所以底层提供了两个宏,方便定义。
比如定长对象浮点数,在底层对应的结构体为 PyFloatObject,它只需在 PyObject 的基础上再加一个 double 即可。
再比如变长对象列表,在底层对应的结构体是 PyListObject,所以它需要在 PyVarObject 的基础上再加一个指向指针数组首元素的二级指针和一个容量。
这上面的每一个字段都代表什么,我们之前提到过,当然这些内置的数据结构后续还会单独剖析。 里面的 ob_item 就是指向指针数组首元素的二级指针,而 allocated 表示已经分配的容量,一旦添加元素的时候发现 ob_size 自增 1 之后会大于 allocated,那么解释器就知道数组已经满了(容量不够了)。于是会申请一个长度更大的指针数组,然后将旧数组内部的元素按照顺序逐个拷贝到新数组里面去,并让 ob_item 指向新数组的首元素,这个过程就是列表的扩容,后续在剖析列表的时候还会细说。 所以我们看到列表在添加元素的时候,地址是不会改变的,即使容量不够了也没有关系,直接让 ob_item 指向新的数组就好了,至于 PyListObject 对象本身的地址是不会变化的。 小结 PyObject 是 Python 对象的核心,因为 Python 对象在 C 的层面就是一个结构体,并且所有的结构体都嵌套了 PyObject 结构体。而 PyObject 内部有引用计数和类型这两个字段,因此我们可以肯定的说 Python 的任何一个对象都有引用计数和类型这两个属性。 另外大部分对象都有长度的概念,所以 PyObject 再加上长度就诞生出了 PyVarObject,它在 PyObject 的基础上添加了一个 ob_size 字段,用于描述对象的长度。比如字符串内部的 ob_size 维护的是字符串的字符个数,元组、列表、字典等等,其内部的 ob_size 维护的是存储的元素个数,所以使用 len 函数获取对象长度是一个 O(1) 的操作。 本文参考自:
|
|