Quantcast
Viewing all articles
Browse latest Browse all 217

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

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 为例),其目录结构如下:Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

在上述命令中,你需要下载并安装一些工具,包括 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 对象构造器的基石,我们来看看它的定义:Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

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

(二)PyTypeObject 类型对象

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

事实上从名字上你也能看出来这每一个成员代表的含义,与我们在 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 对象为例,通过源码的大致步骤了解它的两种创建过程。首先先看浮点数的定义:

Image may be NSFW.
Clik here to view.

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

使用泛型 API 创建

Image may be NSFW.
Clik here to view.
Image may be NSFW.
Clik here to view.
Image may be NSFW.
Clik here to view.
Image may be NSFW.
Clik here to view.

使用特型 API 创建

Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.
比如创建列表:可以使用 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

Viewing all articles
Browse latest Browse all 217

Trending Articles