6.3 从输入构建模块
Last updated
Last updated
任何代码在被执行之前,必须从输入编译成一个模块。正如之前所讨论的那样,输入的类型可以有多种:
本地的文件和包;
输入/输出流,如:标准输入或内存管道;
字符串。
读取输入的内容并传递给解析器,然后再传递给编译器:
正是由于输入类型的灵活性,因此有很大一部分的 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 有了运行时配置和命令行参数,它就可以加载它所需要执行的代码,这个任务由 Modules/main.c
文件中的 pymain_main() 函数来完成。
CPython 现在将使用新创建的 PyConfig 实例中指定的选项来执行所提供的代码。
CPython 可以通过指定 -c
选项,从而通过命令行模式来执行一个小的 Python 应用,如:思考一下当你执行 print(2 ** 2)
时会发生什么:
首先,pymain_run_command() 函数在 Modules/main.c
文件中被执行,其将命令行中通过 -c
传入的命令行作为一个 wchar_t*
类型的参数。
注
wchar_t*
类型通常作为 CPython 中 Unicode 数据的底层存储类型,因为这种类型的大小可以存储 UTF-8 字符。
当 wchar_t
类型转换成 Python 字符串时,Objects/unicodeobject.c
文件中有一个叫做 PyUnicode_FromWideChar() 的辅助函数会返回 Unicode 字符串,之后由 PyUnicode_AsUTF8String()
完成 UTF-8 编码。
Python 的 Unicode 字符串在“Unicode 字符串类型”章节和“对象与类型”章节有深入的介绍。
完成此操作后,pymain_run_command() 将会把 Python 字节对象传递给 PyRun_SimpleStringFlags() 用于执行。
PyRun_SimpleStringFlags() 函数是 Python/pythonrun.c
文件的一个部分,它的目的是将一个字符串转换成 Python 模块,然后把它送去执行。
每个 Python 模块需要有入口点(__main__
)才能被作为独立模块来执行。 PyRun_SimpleStringFlags()
函数将会隐式地创建入口点。
一旦 PyRun_SimpleStringFlags() 创建了模块和字典,它就会调用 PyRun_StringFlags() 。PyRun_SimpleStringFlags() 创建了一个假的文件名,然后调用 Python 解析器从字符串创建一个抽象语法树( AST )并返回一个模块。你将在下一章了解有关 AST 的更多信息。
注
Python 模块是用于将解析后的代码交给编译器的数据结构。Python 模块的 C 数据结构名为 mod_ty
,并定义在 Include/Python-ast.h
文件中。
执行 Python 命令的另一种方式是通过 -m
选项与模块名一起使用,一个典型的例子是通过 python -m unittest
,其在标准库中运行 unittest
模块。
以脚本的形式来执行模块这个想法最初是在 PEP 338 中被提出的,明确的相对导入的标准是在 PEP 366 中定义的。
-m
标志意味着你想要执行模块包中入口点(__main__
)中的所有内容,它也表示你要在 sys.path
中搜索的命名模块。
正是由于导入库(importlib
)中的这种搜索机制,因此你不需要记住 unittest
模块在文件系统中所存储的位置。
CPython 导入一个标准库模块 runpy
并通过 PyObject_Call() 来执行它,导入的过程由一个名为 PyImport_ImportModule() 的 C API 函数完成,该函数定义在 Python/import.c
文件中。
注
在 Python 中,如果你有一个对象并且想要获取其属性,那么可以调用 getattr()
。而在 C API 中,需要调用的是 PyObject_GetAttrString(),该方法在 Objects/object.c 文件中定义。如果你想运行一个可调用的方法,那么你可以给它加上括号,或者你也可以在任何 Python 对象上调用 call()
属性。call()
方法在 Objects/object.c
文件中实现:
runpy
模块定义在 Lib/runpy.py 文件中,是用纯 Python 编写的。
执行 python -m <module>
相当于运行 python -m runpy <module>
。 创建 runpy
模块是为了将操作系统上定位和执行模块的过程抽象出来。 为了运行目标模块,runpy
做了以下三件事:
为你指定的模块名调用 __import__()
方法;
将 __name__
(模块名称)设置到名为 __main__
的命名空间;
在 __main__
命名空间中执行模块。
runpy
模块还支持执行目录和 zip 文件。
如果 python 执行时的第一个参数是一个文件名,例如 python test.py
,CPython 将会打开一个文件句柄并将句柄传递给 PyRun_SimpleFileExFlags(),该方法在 Python/pythonrun.c 文件中定义。 这个方法可以处理三种类型的文件路径:
如果文件路径是 .pyc
文件,那么它将会调用 run_pyc_file();
如果文件路径是脚本文件(.py
),那么它将会调用 PyRun_FileExFlags();
如果文件路径是 stdin
,如:用户执行了 <command> | python
,那么它会将 stdin
作为一个文件句柄并调用 PyRun_FileExFlags()。
对于 stdin
和基本的脚本文件,CPython 会将文件句柄传递给 Python/pythonrun.c 文件中定义的 PyRun_FileExFlags() 函数。
PyRun_FileExFlags() 的目的类似于 PyRun_SimpleStringFlags()。CPython 会将文件句柄加载到PyParser_ASTFromFileObject() 中。
与 PyRun_SimpleStringFlags() 相同,一旦 PyRun_FileExFlags() 从文件中创建了一个 Python 模块,就会将此模块发给 run_mod() 去执行。
如果用户运行带有 .pyc
文件路径的 python 可执行程序,那么 CPython 不会将文件作为纯文本文件加载并解析它,而是假定 .pyc
文件包含一个写入磁盘的 code object
。
在 PyRun_SimpleFileExFlags() 中,有一个子句用于用户提供 .pyc
文件的文件路径。 Python/pythonrun.c
文件中的 run_pyc_file() 函数使用文件句柄从 .pyc
文件中反序列化(marshal)为 code object
。
磁盘上的 code object
数据结构是 CPython 编译器缓存已编译代码的方式,这样它就不需要在调用脚本时都去解析一次。
注
Marshaling 的意思是将一个文件的内容复制到内存并将它们转换为特定的数据结构。
一旦 code object
被反序列化到内存中,它就会被送到 run_eval_code_obj(),该函数接着会调用 Python/ceval.c
来执行代码。