当前位置:首页 > 科技  > 软件

万丈高楼平地起,一切从 PyObject 开始

来源: 责编: 时间:2024-05-16 09:09:28 294观看
导读楔子在前面的文章中我们说到,面向对象理论中的类和对象这两个概念在 Python 内部都是通过对象实现的。类是一种对象,称为类型对象,类实例化得到的也是对象,称为实例对象。但是对象在 Python 的底层是如何实现的呢?Python

楔子

F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

在前面的文章中我们说到,面向对象理论中的类和对象这两个概念在 Python 内部都是通过对象实现的。类是一种对象,称为类型对象,类实例化得到的也是对象,称为实例对象。F4c28资讯网——每日最新资讯28at.com

但是对象在 Python 的底层是如何实现的呢?Python 解释器是基于 C 语言实现的 ,但 C 并不是一个面向对象的语言,那么它是如何实现 Python 的面向对象的呢?F4c28资讯网——每日最新资讯28at.com

首先对于人的思维来说,对象是一个比较形象的概念,但对于计算机来说,对象却是一个抽象的概念。它并不能理解这是一个整数,那是一个字符串,计算机所知道的一切都是字节。F4c28资讯网——每日最新资讯28at.com

通常的说法是:对象是数据以及基于这些数据所能进行的操作的集合。在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也可能是离散的。F4c28资讯网——每日最新资讯28at.com

而 Python 的任何对象在 C 中都对应一个结构体实例,在 Python 中创建一个对象,等价于在 C 中创建一个结构体实例。所以 Python 的对象,其本质就是 C 的 malloc 函数为结构体实例在堆区申请的一块内存。F4c28资讯网——每日最新资讯28at.com

下面我们就来分析一下对象在 C 中是如何实现的。F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

对象的地基:PyObject

F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

Python 一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息位于 PyObject 中,它是 Python 对象机制的核心,下面来看看它的定义。F4c28资讯网——每日最新资讯28at.com

注:我们整个系列的源码都是 3.12 版本的。F4c28资讯网——每日最新资讯28at.com

// Include/pytypedefs.htypedef struct _object PyObject;

我们看到具体定义位于 struct _object 中,PyObject 只是它的别名。F4c28资讯网——每日最新资讯28at.com

// Include/object.hstruct _object {    _PyObject_HEAD_EXTRA    union {       Py_ssize_t ob_refcnt;       PY_UINT32_T ob_refcnt_split[2];    };    PyTypeObject *ob_type;};

注:源码中定义的 struct _object 看起来会更复杂一些,因为里面还包含了一些宏判断,用于适配不同的操作系统和编译器。F4c28资讯网——每日最新资讯28at.com

图片图片F4c28资讯网——每日最新资讯28at.com

这些宏判断我们不需要关注,对于当前的 64 位机器来说,等价于如下。F4c28资讯网——每日最新资讯28at.com

// Include/object.hstruct _object {    _PyObject_HEAD_EXTRA    union {       Py_ssize_t ob_refcnt;       PY_UINT32_T ob_refcnt_split[2];    };    PyTypeObject *ob_type;};

然后是 _PyObject_HEAD_EXTRA,它也是一个宏,定义如下。F4c28资讯网——每日最新资讯28at.com

// Include/object.h// 如果定义了宏 Py_TRACE_REFS#ifdef Py_TRACE_REFS// 那么 _PyObject_HEAD_EXTRA 会展开成如下两个字段// 显然程序中创建的对象会组成一个双向链表#define _PyObject_HEAD_EXTRA      /    PyObject *_ob_next;           /    PyObject *_ob_prev;// 用于将 _ob_next 和 _ob_prev 初始化为空#define _PyObject_EXTRA_INIT _Py_NULL, _Py_NULL,// 否则说明没有定义宏 Py_TRACE_REFS// 那么 _PyObject_HEAD_EXTRA 和 _PyObject_EXTRA_INIT 不会有任何作用#else#  define _PyObject_HEAD_EXTRA#  define _PyObject_EXTRA_INIT#endif

关于 PyObject 的定义,再画一张图总结一下。F4c28资讯网——每日最新资讯28at.com

图片图片F4c28资讯网——每日最新资讯28at.com

Py_TRACE_REFS 一般只在编译调试的时候会开启,我们从官网下载的都是 Release 版本,不包含这个宏,因此这里我们也不考虑它。F4c28资讯网——每日最新资讯28at.com

所以 PyObject 最终就等价于下面这个样子:F4c28资讯网——每日最新资讯28at.com

// Include/object.hstruct _object {    union {       Py_ssize_t ob_refcnt;       PY_UINT32_T ob_refcnt_split[2];    };    PyTypeObject *ob_type;};// Include/pytypedefs.htypedef struct _object PyObject;

当然这两者也可以写在一起,即定义结构体的同时起一个别名。F4c28资讯网——每日最新资讯28at.com

typedef struct _object {    union {       Py_ssize_t ob_refcnt;       PY_UINT32_T ob_refcnt_split[2];    };    PyTypeObject *ob_type;} PyObject;

方式是等价的,只不过 Python 将两者分开了,并写在了不同的文件中。F4c28资讯网——每日最新资讯28at.com

了解了 PyObject 的结构之后,我们再来看一下它内部的字段。F4c28资讯网——每日最新资讯28at.com

ob_refcnt:引用计数F4c28资讯网——每日最新资讯28at.com

ob_refcnt 表示对象的引用计数,当对象被引用时,ob_refcnt 会自增 1;引用解除时,ob_refcnt 会自减 1。而当对象的引用计数为 0 时,则会被回收。F4c28资讯网——每日最新资讯28at.com

那么在哪些情况下,引用计数会加 1 呢?哪些情况下,引用计数会减 1 呢?F4c28资讯网——每日最新资讯28at.com

导致引用计数加 1 的情况:F4c28资讯网——每日最新资讯28at.com

  • 对象被创建:比如 name = "古明地觉",此时对象就是 "古明地觉" 这个字符串, 创建成功时它的引用计数为 1;
  • 变量传递使得对象被新的变量引用:比如 name2 = name;
  • 引用该对象的某个变量作为参数传到一个函数或者类中:比如 func(name);
  • 引用该对象的某个变量作为元组、列表、集合等容器的元素:比如 lst = [name];

导致引用计数减 1 的情况:F4c28资讯网——每日最新资讯28at.com

  • 引用该对象的变量被显式地销毁:del name;
  • 引用该对象的变量指向了别的对象:name = "";
  • 引用该对象的变量离开了它的作用域,比如函数的局部变量在函数执行完毕的时候会被删除;
  • 引用该对象的变量所在的容器被销毁,或者变量从容器里面被删除;

因为变量只是一个和对象绑定的符号,接地气一点的说法就是变量是个便利贴,贴在指定的对象上面。所以 del 变量 并不是删除变量指向的对象,而是删除变量本身,可以理解为将对象身上的便利贴给撕掉了,其结果就是对象的引用计数减一。F4c28资讯网——每日最新资讯28at.com

至于对象是否被删除(回收)则是解释器判断引用计数是否为 0 决定的,为 0 就删,不为 0 就不删,就这么简单。F4c28资讯网——每日最新资讯28at.com

然后需要强调的是,在 3.12 之前的 Python 源码中,PyObject 是这么定义的,以 3.8 为例。F4c28资讯网——每日最新资讯28at.com

图片图片F4c28资讯网——每日最新资讯28at.com

在 3.12 之前,引用计数通过一个 ob_refcnt 字段来维护,字段类型为 Py_ssize_t,它是 ssize_t 的别名,在 64 位机器上等价于 int64。因此一个对象的引用计数不能超过 int64 所表示的最大范围。但很明显,如果不费九牛二虎之力去写恶意代码,是不可能超过这个范围的。F4c28资讯网——每日最新资讯28at.com

还是很好理解的,但从 3.12 开始,却搞了个共同体(union)出来,这是为啥呢?因为 Python 从 3.12 开始引入了一个概念叫永恒对象。顾名思义,永恒对象就是那些永远不会被回收的对象。F4c28资讯网——每日最新资讯28at.com

// Include/object.h#define _Py_IMMORTAL_REFCNT UINT_MAX

永恒对象的引用计数为 uint32 类型的最大值,即 2 的 32 次方减 1,像 None、-5 到 256 之间的小整数,都属于永恒对象。F4c28资讯网——每日最新资讯28at.com

图片图片F4c28资讯网——每日最新资讯28at.com

共同体中的 ob_refcnt 字段的作用还和之前一样,依旧是负责维护对象的引用计数。F4c28资讯网——每日最新资讯28at.com

但 ob_refcnt_split 也会维护一份引用计数,它是 uint32 类型的数组,长度为 2,但只会用数组的一个元素来维护。如果发现对象的引用计数达到了 uint32 的最大值,那么会将对象判定为永恒对象,而永恒对象永远不会被回收。F4c28资讯网——每日最新资讯28at.com

所以 ob_refcnt_split 是针对永恒对象引入的,它是一个长度为 2 的 uint32 类型的数组,大小是 8 字节。而 ob_refcnt 是 Py_ssize_t 类型,等价于 int64,大小也是 8 字节。由于这两者组成的是共同体,所以整体大小依旧是 8 字节,因此 PyObject 结构体实例的大小和之前一样。F4c28资讯网——每日最新资讯28at.com

当然啦,虽然引用计数是由共同体来维护,但你把它当成普通的 Py_ssize_t 类型的字段来理解也是可以的。因为 3.12 之前只有一个 ob_refcnt,而 ob_refcnt_split 是针对永恒对象专门引入的。F4c28资讯网——每日最新资讯28at.com

ob_type:类型指针F4c28资讯网——每日最新资讯28at.com

对象是有类型的,类型对象描述实例对象的行为,而 ob_type 存储的便是对应类型对象的指针,所以类型对象在底层是一个 PyTypeObject 结构体实例。F4c28资讯网——每日最新资讯28at.com

从这里可以看出,所有的类型对象在底层都是由同一个结构体实例化得到的,因为 PyObject 是所有对象共有的,它们的 ob_type 指向的都是 PyTypeObject。F4c28资讯网——每日最新资讯28at.com

所以不同的实例对象对应不同的结构体,但是类型对象对应的都是同一个结构体。F4c28资讯网——每日最新资讯28at.com

以上就是 PyObject,它的定义非常简单,就一个引用计数和一个类型对象的指针。这两个字段的大小都是 8 字节,所以一个 PyObject 结构体实例的大小是 16 字节。F4c28资讯网——每日最新资讯28at.com

另外,由于 PyObject 是所有对象都具有的,换句话说就是所有对象对应的结构体内部都内嵌了 PyObject,因此你在 Python 里面看到的任何一个对象都有引用计数和类型这两个属性。F4c28资讯网——每日最新资讯28at.com

>>> num = 666  >>> sys.getrefcount(num)2>>> num.__class__<class 'int'>>>> sys.getrefcount(sys)72>>> 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__ 查看。F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

可变对象的地基:PyVarObject

F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

PyObject 是所有对象的核心,它包含了所有对象都共有的信息,但是还有那么一个属性虽然不是每个对象都有,但至少有一大半的对象会有,能猜到是什么吗?F4c28资讯网——每日最新资讯28at.com

之前说过,对象根据所占的内存是否固定,可以分为定长对象和变长对象,而变长对象显然有一个长度的概念,比如字符串、列表、元组等等。即便是相同类型的实例对象,但是长度不同,所占的内存也是不同的。F4c28资讯网——每日最新资讯28at.com

比如字符串内部有多少个字符,元组、列表内部有多少个元素,显然这里的多少也是 Python 中很多对象的共有特征。虽然不像引用计数和类型那样是每个对象都必有的,但也是绝大部分对象所具有的。F4c28资讯网——每日最新资讯28at.com

所以针对变长对象,Python 底层也提供了一个结构体,因为 Python 里面很多都是变长对象。F4c28资讯网——每日最新资讯28at.com

// Include/object.htypedef struct {    PyObject ob_base;    Py_ssize_t ob_size;} PyVarObject;

我们看到 PyVarObject 实际上是 PyObject 的一个扩展,它在 PyObject 的基础上提供了一个 ob_size 字段,用于记录内部的元素个数。比如列表,列表的 ob_size 维护的就是列表的元素个数,插入一个元素,ob_size 会加 1,删除一个元素,ob_size 会减 1。F4c28资讯网——每日最新资讯28at.com

因此使用 len 函数获取列表的元素个数是一个时间复杂度为 O(1) 的操作,因为 ob_size 始终和内部的元素个数保持一致,所以会直接返回 ob_size。F4c28资讯网——每日最新资讯28at.com

所有的变长对象都拥有 PyVarObject,而所有的对象都拥有 PyObject,这就使得在 Python 中,对对象的引用变得非常统一。我们只需要一个 PyObject * 就可以引用任意一个对象,而不需要管这个对象实际是一个什么样的对象。F4c28资讯网——每日最新资讯28at.com

所以 Python 变量、以及容器内部的元素,本质上都是一个 PyObject *。而在操作变量的时候,也要先根据 ob_type 字段判断指向对象的类型,然后再寻找该对象具有的方法,这也是 Python 效率慢的原因之一。F4c28资讯网——每日最新资讯28at.com

由于 PyObject 和 PyVarObject 要经常被使用,所以底层提供了两个宏,方便定义。F4c28资讯网——每日最新资讯28at.com

// Include/object.h#define PyObject_HEAD    PyObject ob_base;#define PyObject_VAR_HEAD    PyVarObject ob_base;

比如定长对象浮点数,在底层对应的结构体为 PyFloatObject,它只需在 PyObject 的基础上再加一个 double 即可。F4c28资讯网——每日最新资讯28at.com

typedef struct {    // 等价于 PyObject ob_base;    PyObject_HEAD    double ob_fval;} PyFloatObject;

再比如变长对象列表,在底层对应的结构体是 PyListObject,所以它需要在 PyVarObject 的基础上再加一个指向指针数组首元素的二级指针和一个容量。F4c28资讯网——每日最新资讯28at.com

typedef struct {    PyObject_VAR_HEAD    PyObject **ob_item;    Py_ssize_t allocated;} PyListObject;

这上面的每一个字段都代表什么,我们之前提到过,当然这些内置的数据结构后续还会单独剖析。F4c28资讯网——每日最新资讯28at.com

里面的 ob_item 就是指向指针数组首元素的二级指针,而 allocated 表示已经分配的容量,一旦添加元素的时候发现 ob_size 自增 1 之后会大于 allocated,那么解释器就知道数组已经满了(容量不够了)。于是会申请一个长度更大的指针数组,然后将旧数组内部的元素按照顺序逐个拷贝到新数组里面去,并让 ob_item 指向新数组的首元素,这个过程就是列表的扩容,后续在剖析列表的时候还会细说。F4c28资讯网——每日最新资讯28at.com

所以我们看到列表在添加元素的时候,地址是不会改变的,即使容量不够了也没有关系,直接让 ob_item 指向新的数组就好了,至于 PyListObject 对象本身的地址是不会变化的。F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

小结

F4c28资讯网——每日最新资讯28at.com

F4c28资讯网——每日最新资讯28at.com

PyObject 是 Python 对象的核心,因为 Python 对象在 C 的层面就是一个结构体,并且所有的结构体都嵌套了 PyObject 结构体。而 PyObject 内部有引用计数和类型这两个字段,因此我们可以肯定的说 Python 的任何一个对象都有引用计数和类型这两个属性。F4c28资讯网——每日最新资讯28at.com

另外大部分对象都有长度的概念,所以 PyObject 再加上长度就诞生出了 PyVarObject,它在 PyObject 的基础上添加了一个 ob_size 字段,用于描述对象的长度。比如字符串内部的 ob_size 维护的是字符串的字符个数,元组、列表、字典等等,其内部的 ob_size 维护的是存储的元素个数,所以使用 len 函数获取对象长度是一个 O(1) 的操作。F4c28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-88377-0.html万丈高楼平地起,一切从 PyObject 开始

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 一次炫技差点引发的惨案

下一篇: 常见,但是总回答不好的面试题:JS 模块化以及模块打包器

标签:
  • 热门焦点
  • 5月安卓手机好评榜:魅族20 Pro夺冠

    性能榜和性价比榜之后,我们来看最后的安卓手机好评榜,数据来源安兔兔评测,收集时间2023年5月1日至5月31日,仅限国内市场。第一名:魅族20 Pro好评率:97.50%不得不感慨魅族老品牌还
  • K6:面向开发人员的现代负载测试工具

    K6 是一个开源负载测试工具,可以轻松编写、运行和分析性能测试。它建立在 Go 和 JavaScript 之上,它被设计为功能强大、可扩展且易于使用。k6 可用于测试各种应用程序,包括 Web
  • 掘力计划第 20 期:Flutter 混合开发的混乱之治

    在掘力计划系列活动第20场,《Flutter 开发实战详解》作者,掘金优秀作者,Github GSY 系列目负责人恋猫的小郭分享了Flutter 混合开发的混乱之治。Flutter 基于自研的 Skia 引擎
  • Automa-通过连接块来自动化你的浏览器

    1、前言通过浏览器插件可实现自动化脚本的录制与编写,具有代表性的工具就是:Selenium IDE、Katalon Recorder,对于简单的业务来说可快速实现自动化的上手工作。Selenium IDEKat
  • 三分钟白话RocketMQ系列—— 如何发送消息

    我们知道RocketMQ主要分为消息 生产、存储(消息堆积)、消费 三大块领域。那接下来,我们白话一下,RocketMQ是如何发送消息的,揭秘消息生产全过程。注意,如果白话中不小心提到相关代
  • Python异步IO编程的进程/线程通信实现

    这篇文章再讲3种方式,同时讲4中进程间通信的方式一、 Python 中线程间通信的实现方式共享变量共享变量是多个线程可以共同访问的变量。在Python中,可以使用threading模块中的L
  • 认真聊聊东方甄选:如何告别低垂的果实

    来源:山核桃作者:财经无忌爆火一年后,俞敏洪和他的东方甄选依旧是颇受外界关心的&ldquo;网红&rdquo;。7月5日至9日,为期5天的东方甄选&ldquo;甘肃行&rdquo;首次在自有App内直播,
  • 华为Mate60标准版细节曝光:经典星环相机模组回归

    这段时间以来,关于华为新旗舰的爆料日渐密集。据此前多方爆料,今年华为将开始恢复一年双旗舰战略,除上半年推出的P60系列外,往年下半年的Mate系列也将
  • 三星Galaxy Z Fold5官方渲染图曝光:13.4mm折叠厚度依旧感人

    据官方此前宣布,三星将于7月26日在韩国首尔举办Unpacked活动,届时将带来带来包括Galaxy Buds 3、Galaxy Watch 6、Galaxy Tab S9、Galaxy Z Flip 5、
Top