写点什么

Python 源码剖析:深度探索 Cpython 对象 - 达观数据

作者:NLP资深玩家
  • 2023-07-13
    上海
  • 本文字数:3667 字

    阅读完需:约 12 分钟

CPython 是 Python 社区的标准,其他版本的 Python,比如 pypy,都会遵行 CPython 的标准 API 实现。想要更深入的认识 Python,就需要了解 CPython 的源码实现。本文将从 CPython 的对象构造器开始入手,带大家揭开 CPython 源码的面纱,带你进入 C + Python 的世界。文章的最后,你也会对 Python 中最重要的概念:一切皆对象 (Object) 有更深刻的认识;你还会发现一些具体的线索,为什么 Python 用起来比其他静态类型语言慢很多。


一、为什么要学习 Python 源码

Python 是一门上层语言,创建者通过有意设计来隐藏背后复杂的细节 (builtins)。在解决项目问题时,很多问题也许能通过搜索引擎找到答案,但 Python 是一门迭代速度非常快的语言,搜索引擎与专业书难以获得实效性好且准确的答案,因此多了解其架构与核心原理,可以更好地理解 Python 语言的使用方式、提高编程技能和调试能力。


二、CPython 整体架构

CPython 整体架构大致分为三个模块:

  1. 代码文件 File Groups - Python 所提供的的大量的模块、库、以及用户自定义的模块。用户还可以通过自定义模块来扩展 Python 系统。


  2. 解释器 Python Core - 又称 Python 虚拟机,对代码分析理解,翻译成字节流,并运行这些字节代码。

    · Scanner 负责词法分析的工作,将代码一行一行切分为 Token

    · Parser 则负责语法分析,将 Token 组织为抽象语法树

    · Compiler 则将语法树转化为指令集合的字节码流

    · Code Evaluator 也是我们常说 Python 虚拟机,负责执行这些字节码


  3. 运行环境 Runtime Env - 包括运行时的对象、基础类型结构、内存分配器和实时的运行状态信息。

    · Object 和 Type Structure 分别是程序在运行过程中生成的对象和 Python 中的自带内建对象,如 Int、Str、List 等

    · Memory Allocator 则负责申请创建对象需要的内存,本质就是封装了 C 语言里面的 malloc() 函数

    · Current State 负责维护运行时的各类状态信息,以便在程序执行过程中如果发生状态变化(正常态和异常态)时,仍然能正常运行

三、编译 CPython

我们可以从下文的 GitHub 地址下载各版本的 CPython 源代码(本文内容以 Python 3.11 为例),其目录结构如下:


接下来,我们将从源代码编译 CPython。此步骤需要 C 编译器和一些构建工具。不同的系统编译方法也不同,这里我用的是 mac 系统。



在上述命令中,你需要下载并安装一些工具,包括 Homebrew,Git,Make, GNU C 编译器和 OpenSSL 等。./configure 步骤用来自动化构建过程,CPPFLAGS 是 c 和 c++ 编译器的选项,这里指定了 zlib 头文件的位置,LDFLAGS 是 gcc 等编译器会用到的一些优化参数,这里是指定了 zlib 库文件的位置,(brew --prefix openssl) 显示的是 openssl 的安装路径,运行完上面命令以后在存储库的根目录中会生成一个 Makefile,你可以通过运行以下命令来构建 CPython 二进制文件。make -j2-j2 标志允许 make 同时运行 2 个作业来加快编译速度。在构建期间,你可能会收到一些错误,例如,dbm,sqlite3,uuid,nis,ossaudiodev,spwd 和 tkinter 将无法使用这组指令构建。如果你不打算针对这些软件包进行开发,这些错误没什么影响。构建将花费几分钟并生成一个名为 python.exe 的二进制文件,虽然它的后缀是 exe 格式,但它确实是 macOS 下的可执行文件。每次改动源代码,都需要重新运行 make 进行编译。


四、了解 Python 对象

(一)PyObject 和 PyVarObject

Python 中一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息就在 PyObject 中,PyObject 是 Python 整个对象机制的核心,是 CPython 对象构造器的基石,我们来看看它的定义:


因此我们看到 PyObject 的定义非常简单,就是一个引用计数和一个类型指针,所以 Python 中的任意对象都必有引用计数和类型这两个属性。针对变长对象,Python 底层也提供了一个结构体,因为 Python 里面很多都是变长对象。我们来看看 PyVarObject 的定义:



例如列表(PyListObject 实例)中的 ob_size 维护的就是列表的元素个数,插入一个元素,ob_size 会加 1,删除一个元素,ob_size 会减 1。因此,我们使用 len 获取列表的元素个数是一个时间复杂度为 O(1) 的操作,因为 ob_size 始终和内部的元素个数保持一致,使用 len 获取元素个数的时候会直接访问 ob_size。

(二)PyTypeObject 类型对象

而将一个对象和其类型对象关联起来的,毫无疑问正是该对象内部的 PyObject 中的 ob_type,也就是类型指针。我们通过对象的 ob_type 成员即可获取类型对象的指针,通过该指针可以获取存储在类型对象中的某些元信息。我们来看看 _typeobject 的几个关键的成员:


事实上从名字上你也能看出来这每一个成员代表的含义,与我们在 Python 中常用的魔法方法很像。而且这里面的成员虽然多,但并非每一个类型对象都具备,比如 int 类型它就没有 tp_as_sequence 和 tp_as_mapping,所以 int 类型的这两个成员的值都是 0。综上所述,Python 底层通过 PyObject 和 PyTypeObject 完成了 C++ 所提供的对象的多态特性。在 Python 中创建一个对象,会分配内存并进行初始化,然后 Python 会用一个 PyObject * 来保存和维护这个对象,因此在 Python 中,变量的传递(包括函数的参数传递)实际上传递的都是一个泛型指针:PyObject *。这个指针具体指向什么类型的对象我们并不知道,只能通过其内部的 ob_type 成员进行动态判断,而正是因为这个 ob_type,Python 实现了多态机制。以变量 a + b 为例,这个 a 和 b 指向的对象可以是整数、浮点数、字符串、列表、元组、甚至是我们自己实现了 add 方法的类的实例对象。因为我们说 Python 中的变量都是一个 PyObject *,所以它可以指向任意的对象,因此 Python 就无法做基于类型方面的优化。首先 Python 底层要通过 ob_type 判断变量指向的对象到底是什么类型,这在 C 的层面上至少需要一次属性查找。然后 Python 将每一个操作都抽象成了一个魔法方法,所以实例相加时要在类型对象中找到该方法对应的函数指针,这又是一次属性查找。找到了之后将 a、b 作为参数传递进去,这会发生一次函数调用,会将对象维护的值拿出来进行运算,然后根据相加的结果创建一个新的对象,再返回其对应的 PyObject * 指针。而对于 C 来讲,由于已经规定好了类型,所以 a + b 在编译之后就是一条简单的机器指令,因此两者在效率上差别很大。

(三)对象的创建与调用

抛出个问题: item = 2.71 和 item = float(2.71) 得到的结果都是 2.71,但它们之间有什么不同呢。或者说列表: lst = [] 和 lst = list()得到的 lst 也都是一个空列表,但这两种方式有什么区别呢?Python 中有许多效果相同,过程不同的表达,值得我们进一步思考。

事实上,Python 内部创建一个对象的方法有两种:

• 通过 Python/C API,可以是泛型 API、也可以是特型 API,用于内置类型

• 通过对应的类型对象去创建,多用于自定义类型 Python 对外提供了 C API,让用户可以从 C 环境中与其交互。由于 Python 解释器是用 C 写成的,所以 Python 内部也在大量使用这些 C API。为了更好的研读源码,系统地了解这些 API 的组成结构是很有必要的,下面以 PyFloatObject 对象为例,通过源码的大致步骤了解它的两种创建过程。首先先看浮点数的定义:



可以看出,PyFloatObject 的结构非常简单,除了 PyObject 这个公共的头部信息之外,只有一个额外的 ob_fval,用于存储具体的值,并且使用的是 C 中的 double。以 f = 3.14 为例,底层结构如下:

使用泛型 API 创建






使用特型 API 创建



综上,不管采用哪种方式创建,最终的关键步骤都是分配内存,创建内置类型的实例对象,Python 是可以直接分配内存的。因为它们有哪些成员在底层都是写死的,Python 对它们了如指掌,因此可以通过 Python/C API 直接分配内存并初始化。以 PyFloat_FromDouble 为例,直接在接口内部为 PyFloatObject 结构体实例分配内存,并初始化相关字段即可。从下文的实验也可以看出,对于内置类型的实例对象而言,使用 Python / C API 创建要快不少。


比如创建列表:可以使用 list()、也可以使用 [ ];创建元组:可以使用 tuple()、也可以使用 ();创建字典:可以使用 dict()、也可以使用 {}。前者是通过类型对象去创建的,后者是通过 Python/C API 创建。但对于内置类型而言,我们推荐使用 Python/C API 创建,会直接解析为对应的 C 一级数据结构,因为这些结构在底层都是已经实现好了的,是可以直接用的,无需通过诸如 list() 这种调用类型对象的方式来创建,因为它们内部还是使用了 Python/C API。

  五、总结     

Python 是一门备受推崇的脚本语言,以其简单的语法和全面的功能而著称,可快速实现各种业务。本文从 CPython 对象构造器入手,介绍了浮点数对象在 CPython 底层数据结构中的表现形式以及对象创建的过程。通过进一步了解 CPython 动态性的实现方式,读者可望在阅读 CPython 源码后提升编写高质量代码的能力。参考资料:

  1. https://github.com/python/cpython

  2. https://docs.python.org/zh-cn/3.11/c-api/index.html

  3. https://jiuaidu.com/jianzhan/990904/

  4. https://www.ab62.cn/article/15965.html

  5. https://zhuanlan.zhihu.com/p/596637636

用户头像

还未添加个人签名 2023-01-06 加入

还未添加个人简介

评论

发布
暂无评论
Python源码剖析:深度探索Cpython对象-达观数据_Python_NLP资深玩家_InfoQ写作社区