CPython 实现原理
  • README
  • 一、简介
    • 1.1 如何使用此书
    • 1.2 额外材料和学习资料
  • 二、获取 CPython 源码
    • 2.1 源代码里有什么?
  • 三、准备你的开发环境
    • 3.1 选IDE还是编辑器?
    • 3.2 安装Visual Studio
    • 3.3 安装Visual Studio Code
    • 3.4 安装JetBrains Clion
    • 3.5 安装Vim
    • 3.6 总结
  • 四、编译 CPython
    • 4.1 在 macOS 上编译 CPython
    • 4.2 在 Linux 上编译 CPython
    • 4.3 安装自定义版本
    • 4.4 make 快速入门
    • 4.5 CPython 的 make 目标
    • 4.6 在 Windows 上编译 CPython
    • 4.7 PGO 优化
    • 4.8 总结
  • 五、Python 语言和语法
    • 5.1 为什么 CPython 是用 C 语言而不是用 Python 语言来实现
    • 5.2 Python 语言规范
    • 5.3 分析器生成器
    • 5.4 重新生成语法
    • 5.5 总结
  • 六、配置和输入
    • 6.1 配置状态
    • 6.2 构建配置
    • 6.3 从输入构建模块
    • 6.4 总结
  • 七、基于语法树的词法分析和解析
    • 7.1 具象语法树生成器
    • 7.2 CPython 解析器-分词器
    • 7.3 抽象语法树
    • 7.4 要记住的术语
    • 7.5 一个示例:添加一个约等于比较运算法
    • 7.6 总结
  • 八、编译器
    • 8.1 相关源文件
    • 8.2 重要的专业术语
    • 8.3 实例化一个编译器
    • 8.4 未来标志和编译器标志
    • 8.5 符号表
    • 8.6 核心编译过程
    • 8.7 汇编
    • 8.8 创建一个 Code Object
    • 8.9 使用 Instaviz 展示 Code Object
    • 8.10 一个示例:实现约等于操作符
    • 8.11 总结
  • 九、求值循环
    • 9.1 构建线程状态
    • 9.2 构建帧对象
    • 9.3 帧的执行
    • 9.4 值栈
    • 9.5 例子:在列表中添加元素
    • 9.6 总结
  • 十、内存管理
    • 10.1 C 中的内存分配
    • 10.2 Python 内存管理系统设计
    • 10.3 CPython 内存分配器
  • 十一、并行和并发
    • 11.1 并行和并发模型
    • 11.2 进程的结构
    • 11.3 多进程并行
    • 11.4 多线程
    • 11.5 异步编程
    • 11.6 生成器
    • 11.7 协程
    • 11.8 异步生成器
    • 11.9 子解释器
    • 11.10 总结
  • 十二、对象和类型
    • 12.1 本章的例子
    • 12.2 内置类型
    • 12.3 对象和可变长度对象类型
    • 12.4 类型类
    • 12.5 布尔和整数类型
    • 12.6 Unicode 字符串类型
    • 12.7 字典类型
    • 12.8 总结
  • 十三、标准库
    • 13.1 Python 模块
    • 13.2 Python 和 C 模块
  • 十四、测试套件
    • 14.1 在 Windows 上运行测试套件
    • 14.2 在 Linux 或 MacOS 上运行测试套件
    • 14.3 测试标志
    • 14.4 运行特定测试
    • 14.5 测试模块
    • 14.6 测试工具
    • 14.7 总结
  • 十五、调试
  • 十六、基准测试、性能分析和追踪
  • 十七、下一步计划
    • 17.1 为 CPython 编写 C 扩展
    • 17.2 改进你的 Python 应用程序
    • 17.3 为 CPython 项目做贡献
    • 17.4 继续学习
  • 十八、附录
    • 18.1 C 预处理器
    • 18.2 基础 C 语法
    • 18.3 总结
  • 致谢
Powered by GitBook
On this page
  • 相关的源文件
  • 读取文件和输入
  • 命令行输入字符串
  • 本地模块输入
  • 来自脚本文件或标准输入的输入
  • 从编译好的字节码输入
Edit on GitHub
  1. 六、配置和输入

6.3 从输入构建模块

Previous6.2 构建配置Next6.4 总结

Last updated 2 years ago

任何代码在被执行之前,必须从输入编译成一个模块。正如之前所讨论的那样,输入的类型可以有多种:

  • 本地的文件和包;

  • 输入/输出流,如:标准输入或内存管道;

  • 字符串。

读取输入的内容并传递给解析器,然后再传递给编译器:

正是由于输入类型的灵活性,因此有很大一部分的 CPython 源码专门用于处理 CPython 分析器的输入。

相关的源文件

用于处理命令行界面的四个主要文件:

文件
用途

Lib/runpy.py

标准库模块,用于导入 Python 模块并执行;

Modules/main.c

为外部代码执行进行函数包装,外部代码来源例如文件、模块或输入流;

Programs/python.c

python 可执行文件的入口,适用于 Windows,Linux 和 macOS 系统,仅用作 Modules/main.c 的装饰器;

Python/pythonrun.c

对内部 C API 进行函数包装以处理来自命令行的输入;

读取文件和输入

CPython 现在将使用新创建的 PyConfig 实例中指定的选项来执行所提供的代码。

命令行输入字符串

CPython 可以通过指定 -c 选项,从而通过命令行模式来执行一个小的 Python 应用,如:思考一下当你执行 print(2 ** 2) 时会发生什么:

$ ./python -c "print(2 ** 2)"

4

注

wchar_t* 类型通常作为 CPython 中 Unicode 数据的底层存储类型,因为这种类型的大小可以存储 UTF-8 字符。

Python 的 Unicode 字符串在“Unicode 字符串类型”章节和“对象与类型”章节有深入的介绍。

每个 Python 模块需要有入口点(__main__)才能被作为独立模块来执行。 PyRun_SimpleStringFlags() 函数将会隐式地创建入口点。

注

Python 模块是用于将解析后的代码交给编译器的数据结构。Python 模块的 C 数据结构名为 mod_ty,并定义在 Include/Python-ast.h 文件中。

本地模块输入

执行 Python 命令的另一种方式是通过 -m 选项与模块名一起使用,一个典型的例子是通过 python -m unittest ,其在标准库中运行 unittest 模块。

-m 标志意味着你想要执行模块包中入口点(__main__)中的所有内容,它也表示你要在 sys.path 中搜索的命名模块。

正是由于导入库(importlib)中的这种搜索机制,因此你不需要记住 unittest 模块在文件系统中所存储的位置。

注

>>> my_str = "hello world!"
>>> my_str.upper()
'HELLO WORLD!'
>>> my_str.upper.__call__()
'HELLO WORLD!'

runpy 模块定义在 Lib/runpy.py 文件中,是用纯 Python 编写的。

执行 python -m <module> 相当于运行 python -m runpy <module>。 创建 runpy 模块是为了将操作系统上定位和执行模块的过程抽象出来。 为了运行目标模块,runpy 做了以下三件事:

  • 为你指定的模块名调用 __import__() 方法;

  • 将 __name__(模块名称)设置到名为 __main__ 的命名空间;

  • 在 __main__ 命名空间中执行模块。

runpy 模块还支持执行目录和 zip 文件。

来自脚本文件或标准输入的输入

从编译好的字节码输入

如果用户运行带有 .pyc 文件路径的 python 可执行程序,那么 CPython 不会将文件作为纯文本文件加载并解析它,而是假定 .pyc 文件包含一个写入磁盘的 code object。

磁盘上的 code object 数据结构是 CPython 编译器缓存已编译代码的方式,这样它就不需要在调用脚本时都去解析一次。

注

Marshaling 的意思是将一个文件的内容复制到内存并将它们转换为特定的数据结构。

一旦 CPython 有了运行时配置和命令行参数,它就可以加载它所需要执行的代码,这个任务由 Modules/main.c 文件中的 函数来完成。

首先, 函数在 Modules/main.c 文件中被执行,其将命令行中通过 -c 传入的命令行作为一个 wchar_t* 类型的参数。

当 wchar_t 类型转换成 Python 字符串时,Objects/unicodeobject.c 文件中有一个叫做 的辅助函数会返回 Unicode 字符串,之后由 PyUnicode_AsUTF8String() 完成 UTF-8 编码。

完成此操作后, 将会把 Python 字节对象传递给 用于执行。

函数是 Python/pythonrun.c 文件的一个部分,它的目的是将一个字符串转换成 Python 模块,然后把它送去执行。

一旦 创建了模块和字典,它就会调用 。 创建了一个假的文件名,然后调用 Python 解析器从字符串创建一个抽象语法树( AST )并返回一个模块。你将在下一章了解有关 AST 的更多信息。

以脚本的形式来执行模块这个想法最初是在 中被提出的,明确的相对导入的标准是在 中定义的。

CPython 导入一个标准库模块 runpy 并通过 来执行它,导入的过程由一个名为 的 C API 函数完成,该函数定义在 Python/import.c 文件中。

在 Python 中,如果你有一个对象并且想要获取其属性,那么可以调用 getattr() 。而在 C API 中,需要调用的是 ,该方法在 Objects/object.c 文件中定义。如果你想运行一个可调用的方法,那么你可以给它加上括号,或者你也可以在任何 Python 对象上调用 call() 属性。call() 方法在 Objects/object.c 文件中实现:

如果 python 执行时的第一个参数是一个文件名,例如 python test.py,CPython 将会打开一个文件句柄并将句柄传递给 ,该方法在 Python/pythonrun.c 文件中定义。 这个方法可以处理三种类型的文件路径:

如果文件路径是 .pyc 文件,那么它将会调用 ;

如果文件路径是脚本文件(.py),那么它将会调用 ;

如果文件路径是 stdin,如:用户执行了 <command> | python,那么它会将 stdin 作为一个文件句柄并调用 。

对于 stdin 和基本的脚本文件,CPython 会将文件句柄传递给 Python/pythonrun.c 文件中定义的 函数。

的目的类似于 。CPython 会将文件句柄加载到 中。

与 相同,一旦 从文件中创建了一个 Python 模块,就会将此模块发给 去执行。

在 中,有一个子句用于用户提供 .pyc 文件的文件路径。 Python/pythonrun.c 文件中的 函数使用文件句柄从 .pyc 文件中反序列化(marshal)为 code object。

一旦 code object 被反序列化到内存中,它就会被送到 ,该函数接着会调用 Python/ceval.c 来执行代码。

pymain_main()
pymain_run_command()
PyUnicode_FromWideChar()
pymain_run_command()
PyRun_SimpleStringFlags()
PyRun_SimpleStringFlags()
PyRun_SimpleStringFlags()
PyRun_StringFlags()
PyRun_SimpleStringFlags()
PEP 338
PEP 366
PyObject_Call()
PyImport_ImportModule()
PyObject_GetAttrString()
PyRun_SimpleFileExFlags()
run_pyc_file()
PyRun_FileExFlags()
PyRun_FileExFlags()
PyRun_FileExFlags()
PyRun_FileExFlags()
PyRun_SimpleStringFlags()
PyParser_ASTFromFileObject()
PyRun_SimpleStringFlags()
PyRun_FileExFlags()
run_mod()
PyRun_SimpleFileExFlags()
run_pyc_file()
run_eval_code_obj()
图6.3.1 输入的传递过程