PEP 432 – 重构 CPython 启动序列
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>, Victor Stinner <vstinner at python.org>, Eric Snow <ericsnowcurrently at gmail.com>
- 讨论至:
- Capi-SIG 列表
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 要求:
- 587
- 创建日期:
- 2012年12月28日
- 发布历史:
- 2012年12月28日, 2013年1月2日, 2019年3月30日, 2020年6月28日
PEP 撤回
从 2012 年末到 2020 年中期,此 PEP 提供了通用背景信息和具体的提案,以使 CPython 的启动序列更易于维护,并使 CPython 运行时更容易作为大型应用程序的一部分进行嵌入。
在此期间的大部分时间里,这些更改要么维护在一个单独的特性分支中,要么作为以下划线前缀的私有 API 存在于 CPython 主存储库中。
在 2019 年,PEP 587 将部分 API 更改迁移到了 Python 3.8+ 的公共 CPython API 中(具体来说,该 PEP 更新了解释器运行时,以提供一个明确的多阶段基于结构体的配置接口)。
2020 年 6 月,为响应指导委员会的询问,PEP 作者决定撤回原始 PEP 是有意义的,因为自 PEP 432 首次编写以来,情况已经发生了很大变化,我们认为对启动序列和嵌入 API 的任何进一步更改,最好通过一个新的 PEP(或 PEPs)来制定,该 PEP(或 PEPs)不仅要考虑 PEP 432 中尚未实现但未被认为足够验证而进入 PEP 587 的想法,还要考虑对公共 PEP 587 API 的任何反馈,以及在调整 CPython 实现以使其更适合嵌入和子解释器方面学到的任何其他经验。
特别是,提出以下更改的 PEP,以及实现这些更改所需的任何进一步的基础设施更改,都可能仍然值得探讨:
- 发布一个替代的 Python 可执行文件,该文件将忽略所有用户级别的设置并默认以隔离模式运行,因此比默认解释器更适合执行系统级别的 Python 应用程序。
- 增强 zipapp 模块,以支持从纯 Python 脚本创建单个文件可执行文件(并且可能包括 Python 扩展模块,鉴于多阶段扩展模块初始化的引入)。
- 将复杂的 sys.path 初始化逻辑从 C 迁移到 Python,以提高测试覆盖率和该代码的整体可维护性。
摘要
此 PEP 提出了一种重构 CPython 启动序列的机制,使其更容易修改参考解释器可执行文件的初始化行为,并使其在创建备用可执行文件或将其嵌入到大型应用程序作为 Python 执行引擎时,更容易控制 CPython 的启动行为。
当此提案的实施完成后,解释器启动将由三个清晰区分且可独立配置的阶段组成:
- Python 核心运行时预初始化
- 内存管理设置
- 确定系统接口使用的编码(包括为后续配置阶段传递的设置)
- Python 核心运行时初始化
- 确保 C API 可用
- 确保内置和冻结模块可访问
- 主解释器配置
- 确保外部模块可访问
- (注意:此阶段的名称很可能更改)
还提出了影响主模块执行和子解释器初始化的更改。
注意:TBC = 待定,TBD = 待定。其中大多数的适当解决方案应在参考实现开发过程中变得更加清晰。
提案
此 PEP 提出将 CPython 运行时的初始化分为三个清晰区分的阶段:
- 核心运行时预初始化
- 核心运行时初始化
- 主解释器配置
(早期版本仅提议两个阶段,但试图将 PEP 实现为内部 CPython 重构的经验表明,至少需要 3 个阶段才能实现清晰的关注点分离。)
拟议的设计也对以下方面有重大影响:
- 主模块执行
- 子解释器初始化
在新设计中,解释器在初始化序列中将经过以下明确定义的阶段:
- 未初始化 - 尚未开始预初始化阶段
- 预初始化 - 没有可用的解释器
- 运行时已初始化 - 主解释器部分可用,子解释器创建尚不可用
- 已初始化 - 主解释器完全可用,子解释器创建可用
PEP 587 是一个更详细的提案,涵盖了将预初始化阶段与后两个阶段分离,但不允许嵌入式应用程序在“运行时已初始化”状态下运行任意代码(相反,初始化核心运行时也将始终完全初始化主解释器,因为这就是 Python 3.8 中原生 CPython CLI 的工作方式)。
作为指导任何设计更改的具体用例,并解决系统实用程序的默认值与用户脚本运行的默认值不同的已知问题,此 PEP 提议创建和分发一个单独的系统 Python(`system-python`)可执行文件,该文件默认以“隔离模式”(由 CPython `-I` 开关选择)运行,以及创建一个示例存根二进制文件,该文件仅运行附加的 zip 存档(允许单个文件的纯 Python 可执行文件),而不是经历正常的 CPython 启动序列。
为控制实现复杂度,此 PEP 不 提议对运行时访问解释器状态的方式进行全面更改。更改现有初始化步骤的顺序以使启动序列更易于维护本身就是一项重大更改,同时进行这些其他更改将使更改更具侵入性且更难审查。然而,此类提案可能适合后续 PEP 或补丁的主题 - 此 PEP 及其相关的子提案的一个关键好处是降低了内部存储模型与配置接口之间的耦合度,因此一旦此 PEP 实现,此类更改应该会更容易。
背景
随着时间的推移,CPython 的初始化序列变得越来越复杂,提供了更多选项,同时也执行更复杂的任务(例如,配置 Python 3 中 OS 接口的 Unicode 设置 [10],引导导入系统的纯 Python 实现,以及实现更适合以提升权限运行的系统应用程序的隔离模式 [6])。
这种复杂性的大部分仅通过 Py_Main 和 Py_Initialize API 进行正式访问,为嵌入式应用程序提供了很少的定制机会。这种蔓延的复杂性也给维护者带来了麻烦,因为大部分配置需要在调用 Py_Initialize 之前完成,这意味着大部分 Python C API 不能安全使用。
有许多提案提出了更复杂的启动行为,例如更好地控制 sys.path 初始化(例如,以跨平台的方式轻松地在命令行中添加其他目录 [7],控制 sys.path[0] 的配置 [8]),在启动 Python 子进程时更容易配置像覆盖跟踪这样的实用程序 [9])。
与其无休止地将这些行为添加到一个已经复杂的系统上,不如此 PEP 提议通过引入更结构化的启动序列来开始简化现状,目的是使这些进一步的功能请求更容易实现。
最初,整个提案都保留在此 PEP 中,但这被证明是不切实际的,因此,随着提案设计的部分稳定下来,它们被拆分到自己的 PEP 中,允许在整体设计细节仍在演变的同时取得进展。
主要关注点
任何对启动序列的更改都需要考虑几个关键问题。
可维护性
截至 Python 3.6,CPython 的启动序列很难理解,更难修改。在许多初始化代码执行期间,解释器的状态并不清楚,这导致了一些行为,例如在调用 Py_Initialize 时(当使用 -X 或 -W 选项时)列表、字典和 Unicode 值已经被创建 [1]。
通过转变为明确的多阶段启动序列,开发人员应该只需要理解:
- 在预配置之前哪些 API 和功能可用(基本上没有,除了预配置 API 本身)
- 在核心运行时配置之前哪些 API 和功能可用,并且如果预配置没有被显式运行,它将隐式地以匹配 Python 3.6 行为的默认设置运行。
- 哪些 API 和功能仅在主解释器完全配置后可用(这希望是 C API 的相对较小的子集)。
这些方面的前两点由 PEP 587 涵盖,而后者区别的细节仍在考虑中。
通过将新设计基于 C 结构体和 Python 数据类型的组合,未来也应该更容易修改系统以添加新的配置选项。
可测试性
CPython 启动序列复杂性带来的一个问题是不同配置设置之间可能的交互的组合爆炸。
这个问题影响了新初始化系统的设计以及拟议的实现方法。
性能
CPython 被广泛用于运行短脚本,其中运行时主要由解释器初始化时间决定。启动序列的任何更改都应尽量减少对启动开销的影响。
importlib 迁移的经验表明,启动时间主要由 I/O 操作决定。但是,为了监控任何更改的影响,可以使用一个简单的基准测试来检查启动和然后拆解解释器所需的时间。
python3 -m timeit -s "from subprocess import call" "call(['./python', '-Sc', 'pass'])"
我系统上 Python 3.7(由 Fedora 项目构建)的当前数字:
$ python3 -m timeit -s "from subprocess import call" "call(['python3', '-Sc', 'pass'])"
50 loops, best of 5: 6.48 msec per loop
(待办:使用 perf 而不是 stdlib timeit 运行此微基准测试)
此 PEP 预计不会对启动时间产生任何重大影响,因为它主要旨在重新排序现有的初始化序列,而不会对单个步骤进行实质性更改。
然而,如果这个简单的检查表明拟议的初始化序列更改可能带来性能问题,那么将开发一个更复杂的微基准测试来协助调查。
必需的配置设置
有关 CPython 解释器配置设置以及可用的各种设置方式的详细列表,请参阅 PEP 587。
实现策略
在 Python 3.4 中曾尝试实现此 PEP 的早期版本 [2],遇到的一个显著问题是,在完成初始结构性更改以开始重构过程后,出现了合并冲突。与 Python 2.5 中转向 AST 编译器或 Python 3.3 中转向 importlib 实现导入系统等其他一些重大更改不同,没有明确的方法可以构建一个不会遭受原始尝试所困扰的合并冲突的草案实现。
因此,实现策略被修改为首先将此重构作为 Python 3.7 的私有 API 实现,然后审查在 Python 3.8 中将新函数和结构公开为公共 API 元素的可能性。
在初始合并之后,Victor Stinner 继续将设置迁移到新结构中,以成功实现 PEP 540 UTF-8 模式的更改(这需要能够跟踪所有先前使用区域编码解码的设置,并改用 UTF-8 重新解码)。Eric Snow 也迁移了许多内部子系统,作为使子解释器功能更健壮的一部分。
这项工作表明,此 PEP 中最初提出的详细设计存在一系列实际问题,因此 Victor 设计并实现了一个改进的私有 API(受到此 PEP 早期迭代的启发),PEP 587 提议将其作为 Python 3.8 的公共 API。
设计细节
注意
这里的 API 细节仍然非常不稳定。显示私有 API 当前状态的头文件主要是:
- https://github.com/python/cpython/blob/master/Include/cpython/coreconfig.h
- https://github.com/python/cpython/blob/master/Include/cpython/pystate.h
- https://github.com/python/cpython/blob/master/Include/cpython/pylifecycle.h
PEP 587 涵盖了被认为足够稳定可以公开的 API 的方面。对于提议的 API,文本下方会添加“(参见 PEP 587)”。
此提案的主要主题是在启动过程的更早阶段初始化核心语言运行时并创建部分初始化的主解释器状态。这将允许在初始化过程的其余部分使用大部分 CPython API,从而可能简化许多当前需要依赖基本 C 功能的操作,而不是能够使用 CPython C API 提供的更丰富的数据结构。
PEP 587 涵盖了该任务的一部分,即将现有“在 Py_Initialize 之前可以调用”接口所需的组件(如内存分配器和操作系统接口编码详细信息)拆分到一个单独的预配置步骤中。
在以下内容中,“嵌入式应用程序”一词也包括标准的 CPython 命令行应用程序。
解释器初始化阶段
提议以下不同的解释器初始化阶段:
- 未初始化
- 严格来说不是一个阶段,而是缺乏一个阶段
Py_IsInitializing()返回0Py_IsRuntimeInitialized()返回0Py_IsInitialized()返回0- 嵌入式应用程序确定要使用的内存分配器,以及要用于访问操作系统接口的编码(或选择将这些决策委托给 Python 运行时)。
- 应用程序通过调用其中一个
Py_PreInitializeAPI(参见 PEP 587)开始初始化过程。
- 运行时预初始化
- 没有可用的解释器
Py_IsInitializing()返回1Py_IsRuntimeInitialized()返回0Py_IsInitialized()返回0- 嵌入式应用程序确定初始化核心 CPython 运行时和创建主解释器所需的设置,并通过调用
Py_InitializeRuntime进入下一阶段。 - 注意:截至 PEP 587,嵌入式应用程序改而调用
Py_Main()、Py_UnixMain或其中一个Py_InitializeAPI,从而直接跳转到已初始化状态。
- 主解释器初始化
- 内置数据类型和其他核心运行时服务可用
- 主解释器可用,但仅部分配置
Py_IsInitializing()返回1Py_IsRuntimeInitialized()返回1Py_IsInitialized()返回0- 嵌入式应用程序通过调用
Py_InitializeMainInterpreter来确定和应用完成初始化过程所需的设置。 - 注意:截至 PEP 587,此状态无法通过任何公共 API 访问,它仅作为
Py_Initialize函数运行期间的隐式内部状态。
- 已初始化
- 主解释器可用且完全运行,但 `
__main__` 相关的元数据不完整 Py_IsInitializing()返回0Py_IsRuntimeInitialized()返回1Py_IsInitialized()返回1
- 主解释器可用且完全运行,但 `
阶段调用
所有列出的阶段都将由标准的 CPython 解释器和提议的系统 Python 解释器使用。
嵌入式应用程序仍可以通过使用现有的 Py_Initialize 或 Py_Main() API,继续让初始化几乎完全由 CPython 控制 - 向后兼容性将得以保留。
或者,如果嵌入式应用程序想要对 CPython 的初始状态有更大的控制权,它将能够使用新的、更精细的 API,允许嵌入式应用程序对初始化过程有更大的控制权。
PEP 587 涵盖了该 API 的初始迭代,将预初始化阶段分离出来,而不尝试将核心运行时初始化与主解释器初始化分开。
未初始化状态
未初始化状态是嵌入式应用程序确定正确传递配置设置给嵌入式 Python 运行时所需的设置的地方。
这包括告诉 Python 使用哪个内存分配器,以及在处理提供的设置时使用哪种文本编码。
PEP 587 在其 PyPreConfig 结构中定义了退出此状态所需的设置。
新的查询 API 将允许代码确定解释器是否尚未开始初始化过程。
int Py_IsInitializing();
查询一个完全未初始化的环境将是 `!(Py_Initialized() || Py_Initializing())`。
运行时预初始化阶段
注意
在 PEP 587 中,此阶段的设置尚未分离,而是仅通过组合的 `PyConfig` 结构可用。
预初始化阶段是嵌入式应用程序确定在 CPython 运行时可以完全初始化之前绝对必需的设置。目前,此类别中的主要配置设置与随机哈希算法相关 - 哈希算法必须在进程生命周期内保持一致,因此它们必须在创建核心解释器之前就到位。
必需的设置包括一个指示是否使用特定种子值进行随机哈希的标志,如果使用,则为种子的特定值(种子值为零会禁用随机哈希)。此外,由于可能使用 `PYTHONHASHSEED` 来配置哈希随机化,因此还必须及早解决是否考虑环境变量的问题。最后,为了支持 CPython 构建过程,提供了一个选项来完全禁用导入系统。
此启动序列步骤的建议 API 是:
PyInitError Py_InitializeRuntime(
const PyRuntimeConfig *config
);
PyInitError Py_InitializeRuntimeFromArgs(
const PyRuntimeConfig *config, int argc, char **argv
);
PyInitError Py_InitializeRuntimeFromWideArgs(
const PyRuntimeConfig *config, int argc, wchar_t **argv
);
如果 `Py_IsInitializing()` 为 false,则 `Py_InitializeRuntime` 函数将隐式调用相应的 `Py_PreInitialize` 函数。将向下传递 `use_environment` 设置,而其他设置将根据其默认值进行处理,如 PEP 587 所述。
返回类型 `PyInitError` 在 PEP 587 中定义,它允许嵌入式应用程序优雅地处理 Python 运行时初始化失败,而不是让整个进程被 `Py_FatalError` abrupt 终止。
新的 `PyRuntimeConfig` 结构体保存了核心运行时初步配置和创建主解释器所需的设置。
/* Note: if changing anything in PyRuntimeConfig, also update
* PyRuntimeConfig_INIT */
typedef struct {
bool use_environment; /* as in PyPreConfig, PyConfig from PEP 587 */
int use_hash_seed; /* PYTHONHASHSEED, as in PyConfig from PEP 587 */
unsigned long hash_seed; /* PYTHONHASHSEED, as in PyConfig from PEP 587 */
bool _install_importlib; /* Needed by freeze_importlib */
} PyRuntimeConfig;
/* Rely on the "designated initializer" feature of C99 */
#define PyRuntimeConfig_INIT {.use_hash_seed=-1}
核心配置设置指针可以为 `NULL`,在这种情况下,默认值如 `PyRuntimeConfig_INIT` 中所述。
宏 `PyRuntimeConfig_INIT` 旨在允许轻松地使用合理的默认值初始化结构体实例。
PyRuntimeConfig runtime_config = PyRuntimeConfig_INIT;
`use_environment` 控制所有 Python 相关环境变量的处理。如果标志为 true,则正常处理 `PYTHONHASHSEED`。否则,所有 Python 特定环境变量都被视为未定义(可能会对某些特定于操作系统的环境变量进行例外处理,例如 Mac OS X 中用于在 App Bundle 和主 Python 二进制文件之间通信的环境变量)。
`use_hash_seed` 控制随机哈希算法的配置。如果为零,则使用具有随机种子的随机哈希。如果为正,则使用 `hash_seed` 中的值来播种随机数生成器。如果在此情况下 `hash_seed` 为零,则完全禁用随机哈希。
如果 `use_hash_seed` 为负(且 `use_environment` 为 true),则 CPython 将检查 `PYTHONHASHSEED` 环境变量。如果环境变量未设置,为空字符串,或设置为值 `"random"`,则将使用具有随机种子的随机哈希。如果环境变量设置为字符串 `"0"`,则禁用随机哈希。否则,哈希种子应为范围 `[0; 4294967295]` 内整数的字符串表示。
为了让嵌入式应用程序更容易地使用 `PYTHONHASHSEED` 处理,并使用不同的数据源,将在 C API 中添加以下辅助函数:
int Py_ReadHashSeed(char *seed_text,
int *use_hash_seed,
unsigned long *hash_seed);
此函数接受 `seed_text` 中的种子字符串,并将其转换为适当的标志和种子值。如果 `seed_text` 为 `NULL`、空字符串或值 `"random"`,则 `use_hash_seed` 和 `hash_seed` 都将设置为零。否则,`use_hash_seed` 将设置为 `1`,并且种子文本将被解释为整数并报告为 `hash_seed`。成功时函数返回零。非零返回值表示错误(最可能是转换整数时)。
`_install_importlib` 设置作为 CPython 构建过程的一部分,用于创建一个完全没有导入功能的解释器。它被认为是 CPython 开发团队专有的(因此有前导下划线),因为当前唯一支持的用例是允许使先前冻结的 `importlib._bootstrap` 的字节码无效的编译器更改,而不会破坏构建过程。
目标是尽可能减小此初始配置级别,以保持不同嵌入式应用程序之间的引导环境一致。如果我们能够创建一个有效的解释器状态而无需该设置,那么该设置应仅出现在完整的 `PyConfig` 结构中,而不是核心运行时配置中。
新的查询 API 将允许代码确定解释器是否处于核心运行时初始化与创建主解释器状态和完成大部分主解释器初始化过程之间的引导状态。
int Py_IsRuntimeInitialized();
当 `Py_IsRuntimeInitialized()` 已经为 true 时再次尝试调用 `Py_InitializeRuntime()` 被报告为用户配置错误。(TBC,因为现有的公共初始化 API 支持多次调用而不会出错,并且只是忽略对任何一次性写入设置的更改。保留该行为而不是尝试使新 API 比旧 API 更严格可能是有意义的。)
由于冻结的字节码现在可以在尚未完全初始化的解释器中合法运行,`sys.flags` 将获得一个新的 `initialized` 标志。
核心运行时已初始化后,主解释器和大部分 CPython C API 应该完全可用,除了:
- 不允许编译(因为解析器和编译器尚未正确配置)
- 不允许创建子解释器
- 不允许创建其他线程状态
- `sys` 模块中的以下属性均不存在或为 `None`:* `sys.path` * `sys.argv` * `sys.executable` * `sys.base_exec_prefix` * `sys.base_prefix` * `sys.exec_prefix` * `sys.prefix` * `sys.warnoptions` * `sys.dont_write_bytecode` * `sys.stdin` * `sys.stdout`
- 文件系统编码尚未定义
- IO 编码尚未定义
- CPython 信号处理程序尚未安装
- 只能导入内置和冻结的模块(由于上述限制)
- `sys.stderr` 设置为使用无缓冲二进制模式的临时 IO 对象
- `sys.flags` 属性存在,但各个标志可能尚未具有最终值。
- `sys.flags.initialized` 属性设置为 `0`
- `warnings` 模块尚未初始化
- `__main__` 模块尚不存在
《待定:确定其他任何值得注意的缺失功能》
此步骤提供的最主要功能将是核心 Python 数据类型,特别是字典、列表和字符串。这允许它们在其余的配置步骤中安全使用(与现状不同)。
此外,当前线程将拥有一个有效的 Python 线程状态,允许将任何进一步的配置数据存储在主解释器对象上,而不是 C 进程全局变量中。
任何对 `Py_InitializeRuntime()` 的调用都必须有一个匹配的 `Py_Finalize()` 调用。在两者之间可以省略调用 `Py_InitializeMainInterpreter()`(例如,如果尝试构建主解释器配置设置失败)。
确定剩余的配置设置
初始化序列的下一步是确定完成过程所需的剩余设置。此时不对解释器状态进行任何更改。此步骤的核心 API 是:
int Py_BuildPythonConfig(
PyConfigAsObjects *py_config, const PyConfig *c_config
);
int Py_BuildPythonConfigFromArgs(
PyConfigAsObjects *py_config, const PyConfig *c_config, int argc, char **argv
);
int Py_BuildPythonConfigFromWideArgs(
PyConfigAsObjects *py_config, const PyConfig *c_config, int argc, wchar_t **argv
);
`py_config` 参数应为 `PyConfigAsObjects` 结构体的指针(该结构体可以是存储在 C 堆栈上的临时结构体)。对于任何已配置的值(即任何非 NULL 指针),CPython 将对提供的值进行健全性检查,但否则会接受它为正确。
使用结构体而不是 Python 字典,因为结构体更容易从 C 操作,支持的字段列表对于给定的 CPython 版本是固定的,并且只需要向 Python 代码公开一个只读视图(这相对直接,得益于为公开 `sys.implementation` 所的基础设施)。
与 `Py_InitializeRuntime` 不同,如果发现配置数据存在问题,此调用将引发 Python 异常并报告错误返回值,而不是返回特定于 Python 的 C 结构。
任何支持的未设置的配置设置将在提供的配置结构中得到适当填充。可以通过在调用 `Py_BuildPythonConfig` 之前设置值来完全覆盖默认配置。然后,提供的值也将用于计算由此值派生的任何其他设置。
或者,可以在调用 `Py_BuildPythonConfig` 之后覆盖设置(这可能很有用,如果嵌入式应用程序想要调整某个设置而不是完全替换它,例如移除 `sys.path[0]`)。
`c_config` 参数是 `PyConfig` 结构体的可选指针,该结构体在 PEP 587 中定义。如果提供,它将优先于直接从环境变量或进程全局状态读取设置。
仅仅读取配置不会影响解释器状态:它只会修改传入的配置结构。设置直到调用 `Py_InitializeMainInterpreter`(见下文)才会应用于正在运行的解释器。
支持的配置设置
解释器配置分为两部分:仅与主解释器相关或必须在主解释器和所有子解释器之间相同的设置,以及可能因子解释器而异的设置。
注意:为方便初始实现,仅指示解释器是否为主解释器的标志将在每个解释器基础上进行配置。在实现过程中,将审查其他字段是否可行地使其成为特定于解释器的。
`PyConfigAsObjects` 结构体镜像 PEP 587 中的 `PyConfig` 结构体,但使用完整的 Python 对象来存储值,而不是 C 级数据类型。它添加了 `raw_argv` 和 `argv` 列表字段,因此后续初始化步骤无需单独接受它们。
字段始终是指向 Python 数据类型的指针,未设置的值用 `NULL` 表示。
typedef struct {
/* Argument processing */
PyListObject *raw_argv;
PyListObject *argv;
PyListObject *warnoptions; /* -W switch, PYTHONWARNINGS */
PyDictObject *xoptions; /* -X switch */
/* Filesystem locations */
PyUnicodeObject *program_name;
PyUnicodeObject *executable;
PyUnicodeObject *prefix; /* PYTHONHOME */
PyUnicodeObject *exec_prefix; /* PYTHONHOME */
PyUnicodeObject *base_prefix; /* pyvenv.cfg */
PyUnicodeObject *base_exec_prefix; /* pyvenv.cfg */
/* Site module */
PyBoolObject *enable_site_config; /* -S switch (inverted) */
PyBoolObject *no_user_site; /* -s switch, PYTHONNOUSERSITE */
/* Import configuration */
PyBoolObject *dont_write_bytecode; /* -B switch, PYTHONDONTWRITEBYTECODE */
PyBoolObject *ignore_module_case; /* PYTHONCASEOK */
PyListObject *import_path; /* PYTHONPATH (etc) */
/* Standard streams */
PyBoolObject *use_unbuffered_io; /* -u switch, PYTHONUNBUFFEREDIO */
PyUnicodeObject *stdin_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stdin_errors; /* PYTHONIOENCODING */
PyUnicodeObject *stdout_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stdout_errors; /* PYTHONIOENCODING */
PyUnicodeObject *stderr_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stderr_errors; /* PYTHONIOENCODING */
/* Filesystem access */
PyUnicodeObject *fs_encoding;
/* Debugging output */
PyBoolObject *debug_parser; /* -d switch, PYTHONDEBUG */
PyLongObject *verbosity; /* -v switch */
/* Code generation */
PyLongObject *bytes_warnings; /* -b switch */
PyLongObject *optimize; /* -O switch */
/* Signal handling */
PyBoolObject *install_signal_handlers;
/* Implicit execution */
PyUnicodeObject *startup_file; /* PYTHONSTARTUP */
/* Main module
*
* If prepare_main is set, at most one of the main_* settings should
* be set before calling PyRun_PrepareMain (Py_ReadMainInterpreterConfig
* will set one of them based on the command line arguments if
* prepare_main is non-zero when that API is called).
PyBoolObject *prepare_main;
PyUnicodeObject *main_source; /* -c switch */
PyUnicodeObject *main_path; /* filesystem path */
PyUnicodeObject *main_module; /* -m switch */
PyCodeObject *main_code; /* Run directly from a code object */
PyObject *main_stream; /* Run from stream */
PyBoolObject *run_implicit_code; /* Run implicit code during prep */
/* Interactive main
*
* Note: Settings related to interactive mode are very much in flux.
*/
PyObject *prompt_stream; /* Output interactive prompt */
PyBoolObject *show_banner; /* -q switch (inverted) */
PyBoolObject *inspect_main; /* -i switch, PYTHONINSPECT */
} PyConfigAsObjects;
`PyInterpreterConfig` 结构体保存可能在主解释器和子解释器之间变化的设置。对于主解释器,这些设置由 `Py_InitializeMainInterpreter()` 自动填充。
typedef struct {
PyBoolObject *is_main_interpreter; /* Easily check for subinterpreters */
} PyInterpreterConfig;
由于这些结构体仅由对象指针组成,因此不需要显式的初始化程序定义 - C99 的默认结构体内存初始化为零就足够了。
完成主解释器初始化
初始化过程的最后一步是实际生效配置设置,并将主解释器的引导过程完成到完全运行。
int Py_InitializeMainInterpreter(const PyConfigAsObjects *config);
与 `Py_BuildPythonConfig` 类似,此调用将在发现配置数据存在问题时引发异常并报告错误返回值,而不是表现出致命错误。(TBC,因为现有的公共初始化 API 支持多次调用而不会出错,并且只是忽略对任何一次性写入设置的更改。保留该行为而不是尝试使新 API 比旧 API 更严格可能是有意义的。)
所有配置设置都是必需的 - 配置结构体应始终通过 `Py_BuildPythonConfig` 传递,以确保其完全填充。
成功调用后 `Py_IsInitialized()` 将变为 true,`Py_IsInitializing()` 将变为 false。上述解释器在仅核心运行时已初始化的阶段的注意事项将不再适用。
再次尝试调用 `Py_InitializeMainInterpreter()` 时,如果 `Py_IsInitialized()` 为 true,则是一个错误。
然而,`__main__` 模块相关的某些元数据可能仍不完整。
- `sys.argv[0]` 可能尚未获得其最终值。
- 当使用 CPython 执行模块或包时,它将是 `-m`。
- 它将与 `sys.path[0]` 相同,而不是 `__main__` 模块的位置,当执行有效的 `sys.path` 条目时(通常是 zip 文件或目录)。
- 否则,它将是准确的。
- 普通脚本运行时,脚本名称。
- `-c`(执行提供的字符串)
- `-` 或空字符串(从 stdin 运行)
- `__main__` 模块中的元数据仍将指示它是一个内置模块。
此函数通常会作为其最终操作(在 `Py_IsInitialized()` 已经设置之后)隐式导入 site。在配置设置中将“enable_site_config”标志设置为 `Py_False` 将禁用此行为,并且在进程中显式执行 `import site` 时,不会产生任何全局状态的副作用。
准备主模块
注意
在 PEP 587 中,`PyRun_PrepareMain` 和 `PyRun_ExecMain` 不单独公开,而是通过一个 `Py_RunMain` API 访问,该 API 既准备又执行 main,然后最终化 Python 解释器。
此子阶段完成 `__main__` 模块相关元数据的填充,而不实际开始执行 `__main__` 模块代码。
它通过调用以下 API 来处理:
int PyRun_PrepareMain();
此操作仅允许主解释器执行,并在从属于子解释器的当前线程调用的线程中调用时引发 `RuntimeError`。
实际处理由配置结构作为主相关设置存储在解释器状态中驱动。
如果 `prepare_main` 为零,此调用将不执行任何操作。
如果 `main_source`、`main_path`、`main_module`、`main_stream` 和 `main_code` 全部为 NULL,此调用将不执行任何操作。
如果设置了 `main_source`、`main_path`、`main_module`、`main_stream` 或 `main_code` 中的多个,则将报告 `RuntimeError`。
如果 `main_code` 已设置,则此调用将不执行任何操作。
如果 `main_stream` 已设置,并且 `run_implicit_code` 也已设置,那么在 `__main__` 命名空间中将读取、编译和执行 `startup_file` 中标识的文件。
如果设置了 `main_source`、`main_path` 或 `main_module`,那么此调用将采取必要步骤来填充 `main_code`。
- 对于 `main_source`,提供的字符串将被编译并保存到 `main_code`。
- 对于 `main_path`:
- 如果提供的路径被识别为有效的 `sys.path` 条目,则将其作为 `sys.path[0]` 插入,`main_module` 设置为 `__main__`,然后继续处理 `main_module`。
- 否则,路径将被读取为 CPython 字节码文件。
- 如果失败,它将被读取为 Python 源文件并编译。
- 在后两种情况下,代码对象将被保存到 `main_code`,并且 `__main__.__file__` 将被适当地设置。
- 对于 `main_module`:
- 任何父包都将被导入。
- 模块的加载器将被确定。
- 如果加载器指示模块是包,则在 `main_module` 末尾添加 `.__main__` 并重试(如果最后一个名称段已经是 `.__main__`,则立即失败)。
- 一旦找到模块源代码,就将编译后的模块代码保存为 `main_code`,并适当地填充 `__main__` 中的以下属性:`__name__`、`__loader__`、`__file__`、`__cached__`、`__package__`。
(注意:本节中描述的行为并非新内容,而是对 CPython 解释器当前行为的描述,已针对新配置系统进行了调整。)
执行主模块
注意
在 PEP 587 中,`PyRun_PrepareMain` 和 `PyRun_ExecMain` 不单独公开,而是通过一个 `Py_RunMain` API 访问,该 API 既准备又执行 main,然后最终化 Python 解释器。
此子阶段涵盖实际 `__main__` 模块代码的执行。
它通过调用以下 API 来处理:
int PyRun_ExecMain();
此操作仅允许主解释器执行,并在从属于子解释器的当前线程调用的线程中调用时引发 `RuntimeError`。
实际处理由配置结构作为主相关设置存储在解释器状态中驱动。
如果 `main_stream` 和 `main_code` 都为 NULL,此调用将不执行任何操作。
如果 `main_stream` 和 `main_code` 都已设置,将报告 `RuntimeError`。
如果 `main_stream` 和 `prompt_stream` 都已设置,主执行将委托给一个新的内部 API。
int _PyRun_InteractiveMain(PyObject *input, PyObject* output);
如果 `main_stream` 已设置而 `prompt_stream` 为 NULL,主执行将委托给一个新的内部 API。
int _PyRun_StreamInMain(PyObject *input);
如果 `main_code` 已设置,主执行将委托给一个新的内部 API。
int _PyRun_CodeInMain(PyCodeObject *code);
主执行完成后,如果设置了 `inspect_main`,或者设置了 `PYTHONINSPECT` 环境变量,那么 `PyRun_ExecMain` 将调用 `_PyRun_InteractiveMain(sys.__stdin__, sys.__stdout__)`。
配置数据的内部存储
解释器状态将被更新,以包含在初始化期间通过将解释器状态对象扩展至少包含 `PyConfigAsObjects` 和 `PyInterpreterConfig` 结构体的嵌入式副本所提供的配置设置的详细信息。
出于调试目的,配置设置将公开为 `sys._configuration` 简单命名空间(类似于 `sys.flags` 和 `sys.implementation`。属性本身将是对应于两个配置设置级别的简单命名空间:
所有解释器活动解释器
字段名称将与配置结构中的名称匹配,但 `hash_seed` 除外,它将被故意排除。
故意选择一个以下划线开头的属性,因为这些配置设置是 CPython 实现的一部分,而不是 Python 语言定义的一部分。如果需要新的设置来支持标准库中的跨实现兼容性,那么这些应该与其他实现一起商定,并公开为 `sys.implementation` 上的新必需属性,如 PEP 421 所述。
这些是初始配置设置的快照。它们在运行时不会被解释器修改(除上述情况外)。
创建和配置子解释器
由于新的配置设置存储在解释器状态中,因此在创建新的子解释器时需要对其进行初始化。这比预期的要棘手,因为 `PyThreadState_Swap(NULL);`(幸运的是,CPython 自己的嵌入式测试会调用它,这使得在开发过程中可以检测到这个问题)。
为了为此提供一个简单的解决方案,PEP 提议添加一个新的 API:
Py_InterpreterState *Py_InterpreterState_Main();
这将是 `Py_InterpreterState_Head()` 的对应项,它报告当前存在的旧解释器,而不是最新的。如果 `Py_NewInterpreter()` 从具有现有线程状态的线程调用,那么该线程的解释器配置将用于初始化新子解释器。如果没有当前线程状态,将使用 `Py_InterpreterState_Main()` 的配置。
虽然现有的 `Py_InterpreterState_Head()` API 也可以使用,但该引用在子解释器创建和销毁时会发生变化,而 `PyInterpreterState_Main()` 将始终指向 `Py_InitializeRuntime()` 中创建的初始解释器状态。
还为嵌入式 API 添加了一个新约束:尝试在仍存在子解释器时删除主解释器现在将是一个致命错误。
稳定的 ABI
此 PEP 中提议的大部分 API 都被排除在稳定 ABI 之外,因为嵌入 Python 解释器涉及的耦合度远高于仅仅编写扩展模块。
唯一新公开的 API 中属于稳定 ABI 的是 `Py_IsInitializing()` 和 `Py_IsRuntimeInitialized()` 查询。
构建时配置
此 PEP 对构建时配置设置的处理方式没有进行任何更改,因此对 `sys.implementation` 的内容或 `sysconfig.get_config_vars()` 的结果没有影响。
向后兼容性
向后兼容性将主要通过确保 `Py_BuildPythonConfig()` 查询以前定义的存储在全局变量和环境变量中的所有配置设置,以及 `Py_InitializeMainInterpreter()` 将受影响的设置写回相关位置来得到保留。
一个已确认的不兼容之处是,一些目前被惰性读取的环境变量可能会在解释器初始化期间被读取一次。随着参考实现的发展,这些将在逐个案例的基础上进行更详细的讨论。目前已知会被动态查找的环境变量是:
- `PYTHONCASEOK`:写入 `os.environ['PYTHONCASEOK']` 将不再动态更改解释器在导入时对文件名大小写差异的处理(TBC)。
- `PYTHONINSPECT`:在 `__main__` 模块执行终止后仍然会检查 `os.environ['PYTHONINSPECT']`。
`Py_Initialize()` 风格的初始化将继续得到支持。它将在内部使用(至少部分)新 API,但将继续表现出与今天相同的行为,确保 `sys.argv` 直到后续的 `PySys_SetArgv` 调用(TBC)才会被填充。所有目前支持在 `Py_Initialize()` 之前调用的 API 将继续如此,并且还将支持在 `Py_InitializeRuntime()` 之前调用。
系统 Python 可执行文件
在以管理员权限执行系统实用程序时,CPython 的许多默认行为是不理想的,因为它们可能允许不受信任的代码以提升的权限运行。最令人头疼的问题是用户站点目录已启用、信任环境变量以及包含已执行文件的目录被放在导入路径的开头。
问题 16499 [6] 添加了一个 `-I` 选项来更改常规 CPython 可执行文件的行为,但这是一种难以发现的解决方案(并且为已经复杂的 CLI 添加了另一个选项)。此 PEP 提议改为添加一个单独的 `system-python` 可执行文件。
目前,提供具有不同默认行为的单独可执行文件将极其难以维护。此 PEP 的目标之一是使替换大部分难以维护的引导代码为更常规的 CPython 代码成为可能,并且还使单独的应用程序更容易利用 `Py_Main` 的关键组件。将此更改包含在 PEP 中是为了避免接受一个理论上听起来不错但在实践中可能出现问题的设计。
干净地支持这种“替代 CLI”是提议的更改以更好地公开决定 CPython 支持的不同执行模式之间核心逻辑的主要原因:
- 脚本执行
- 目录/zip 文件执行
- 命令执行(“-c”开关)
- 模块或包执行(“-m”开关)
- 从 stdin 执行(非交互式)
- 交互式 stdin
实际上实现这一点也可能表明需要一些更好的参数解析基础设施,以便在初始化阶段使用。
开放问题
- `Py_BuildPythonConfig` 和 `Py_InitializeMainInterpreter` 的错误详细信息(随着实现进展,这些应该会变得更清楚)。
实施
参考实现正在作为 CPython 参考解释器内部的私有 API 重构进行开发(因为尝试将其作为独立项目维护被证明是不切实际的)。
PEP 587 提取了提案的一部分,该部分被认为足够稳定,值得提议作为 Python 3.8 的公共 API。
现状(截至 Python 3.6)
过去 20 多年来,配置解释器的现有机制以一种相当临时的方式积累起来,导致了一个相当不一致的接口,文档水平参差不齐。
另请参阅 PEP 587,了解有关现有设置及其处理方式的进一步讨论。
(注意:以下信息中的一些内容可能需要清理并添加到 3.x 的 C API 文档中 - 所有这些都是 CPython 特有的,因此不属于语言参考。)
忽略环境变量
命令行选项 `-E` 允许在初始化 Python 解释器时忽略所有环境变量。嵌入式应用程序可以通过在调用 `Py_Initialize()` 之前设置 `Py_IgnoreEnvironmentFlag` 来启用此行为。
在 CPython 源代码中,`Py_GETENV` 宏隐式检查此标志,如果该标志已设置,则始终返回 `NULL`。
《待定:我相信 PYTHONCASEOK 无论此设置如何都会被检查》 《待定:-E 是否也忽略 Windows 注册表项?》
随机哈希
随机哈希通过命令行选项 `-R`(在 3.3 版本之前)以及 `PYTHONHASHSEED` 环境变量进行控制。
在 Python 3.3 中,只有环境变量仍然相关。它可用于禁用随机哈希(使用种子值 0)或强制使用特定的哈希值(例如,用于测试的可重复性,或在进程之间共享哈希值)。
然而,嵌入式应用程序必须使用 `Py_HashRandomizationFlag` 来显式请求哈希随机化(CPython 在 `Py_Main()` 中设置它,而不是在 `Py_Initialize()` 中)。
新的配置 API 应该使嵌入式应用程序能够轻松地重用 `PYTHONHASHSEED` 处理,并使用其他方式提供的基于文本的配置设置(例如,配置文件或单独的环境变量)。
定位 Python 和标准库
Python 二进制文件和标准库的位置受多种因素的影响。用于执行计算的算法除源代码外未在任何地方记录 [3],[4]。即使该描述也不完整,因为它未能更新 Python 3.3 中添加的虚拟环境支持(在 PEP 405 中详述)。
这些计算受以下调用(在调用 `Py_Initialize()` 之前)和环境变量的影响:
Py_SetProgramName()Py_SetPythonHome()PYTHONHOME
文件系统还会检查 `pyvenv.cfg` 文件(参见 PEP 405),或者在没有找到时,检查 `lib/os.py`(Windows)或 `lib/python$VERSION/os.py` 文件。
Windows 上的 `PREFIX` 和 `EXEC_PREFIX` 的构建时设置以及一些注册表设置也相关。硬编码的回退基于 CPython 源代码树的布局以及在源检出中工作时的构建输出。
配置 sys.path
嵌入式应用程序可以在 `Py_Initialize()` 之前调用 `Py_SetPath()` 来完全覆盖 `sys.path` 的计算。仅允许部分计算并不简单,因为在初始化完成后修改 `sys.path` 意味着这些修改在启动序列期间导入标准库模块时不会生效。
如果在使用 Py_GetPath() 第一次调用(Py_Initialize() 中隐式调用)之前没有使用 Py_SetPath(),那么它将基于上述位置数据计算来构建合适的路径条目,并考虑 PYTHONPATH 环境变量。
<待定:在 Windows 上,注册表也有很多需要处理的事项>
site 模块在启动时会被隐式导入(除非通过 -S 选项禁用),它会向初始路径集添加其他路径,具体描述参见其文档 [5]。
可以使用 -s 命令行选项从添加到列表中的目录中排除用户站点目录。嵌入式应用程序可以通过设置 Py_NoUserSiteDirectory 全局变量来控制这一点。
可以使用以下命令来检查给定系统上给定 Python 可执行文件的默认路径配置:
./python -c "import sys, pprint; pprint.pprint(sys.path)"- 标准配置./python -s -c "import sys, pprint; pprint.pprint(sys.path)"- 禁用用户站点目录./python -S -c "import sys, pprint; pprint.pprint(sys.path)"- 禁用所有站点路径修改
(注意:您可以使用 -m site 代替 -c 来查看类似信息,但这会略有误导,因为它会对所有路径条目调用 os.abspath,使相对路径条目看起来像绝对路径。在 Python 3.3 之前的版本中,使用 site 模块还会导致问题,因为显式导入 site 会执行 -S 选项要避免的路径修改,而在 3.3+ 版本中,将 -m site 与 -S 结合使用目前会失败)
sys.path[0] 的计算相对简单
- 对于普通脚本(Python 源代码或编译的字节码),
sys.path[0]将是包含脚本的目录。 - 对于一个有效的
sys.path条目(通常是 zip 文件或目录),sys.path[0]将是该路径 - 对于交互式会话、从 stdin 运行或使用
-c或-m开关时,sys.path[0]将是空字符串,导入系统将其解释为允许从当前目录导入
配置 sys.argv
与本文档 PEP 中讨论的大多数其他设置不同,sys.argv 不是由 Py_Initialize() 隐式设置的。相反,它必须通过显式调用 Py_SetArgv() 来设置。
CPython 在调用 Py_Initialize() 之后,在 Py_Main() 中调用此函数。 sys.argv[1:] 的计算很简单:它们是脚本名称或 -c 或 -m 选项参数后面的命令行参数。
sys.argv[0] 的计算要复杂一些
- 对于普通脚本(源代码或字节码),它将是脚本名称
- 对于
sys.path条目(通常是 zip 文件或目录),它最初将是 zip 文件或目录名称,但之后会被runpy模块更改为导入的__main__模块的完整路径。 - 对于使用
-m开关指定的模块,它最初将是字符串"-m",但之后会被runpy模块更改为已执行模块的完整路径。 - 对于使用
-m开关指定的包,它最初将是字符串"-m",但之后会被runpy模块更改为已执行包的__main__子模块的完整路径。 - 对于使用
-c执行的命令,它将是字符串"-c" - 对于从 stdin 显式请求的输入,它将是字符串
"-" - 否则,它将是空字符串
嵌入式应用程序必须自行调用 Py_SetArgv。CPython 的相关逻辑包含在 Py_Main() 中,并未单独暴露。但是,runpy 模块在 runpy.run_module 和 runpy.run_path 中提供了大致等效的逻辑。
其他配置设置
待定:更详细地介绍以下内容的初始化
- 完全禁用导入系统
- 初始警告系统状态
sys.warnoptions- (-W 选项, PYTHONWARNINGS)
- 任意扩展选项(例如,自动启用
faulthandler)sys._xoptions- (-X 选项)
- 文件系统编码,由以下组件使用:
sys.getfsencodingos.fsencodeos.fsdecode
- I/O 编码和缓冲,由以下组件使用:
sys.stdinsys.stdoutsys.stderr- (-u 选项, PYTHONIOENCODING, PYTHONUNBUFFEREDIO)
- 是否隐式缓存字节码文件
sys.dont_write_bytecode- (-B 选项, PYTHONDONTWRITEBYTECODE)
- 在不区分大小写的文件系统上是否强制文件名大小写正确
os.environ["PYTHONCASEOK"]
sys.flags中暴露给 Python 代码的其他设置debug(在 pgen 解析器中启用调试输出)inspect(在 __main__ 终止后进入交互式解释器)interactive(将 stdin 视为 tty)optimize(__debug__ 状态,写入 .pyc 或 .pyo,剥离文档字符串)no_user_site(不将用户站点目录添加到 sys.path)no_site(启动期间不隐式导入 site)ignore_environment(在配置期间是否使用环境变量)verbose(启用各种随机输出)bytes_warning(隐式 str/bytes 交互的警告/错误)quiet(即使启用了 verbose 或 stdin 是 tty 且解释器以交互模式启动,也禁用横幅输出)
- 是否安装 CPython 的信号处理器
CPython 的大部分配置目前是通过 C 级全局变量处理的
Py_BytesWarningFlag (-b)
Py_DebugFlag (-d option)
Py_InspectFlag (-i option, PYTHONINSPECT)
Py_InteractiveFlag (property of stdin, cannot be overridden)
Py_OptimizeFlag (-O option, PYTHONOPTIMIZE)
Py_DontWriteBytecodeFlag (-B option, PYTHONDONTWRITEBYTECODE)
Py_NoUserSiteDirectory (-s option, PYTHONNOUSERSITE)
Py_NoSiteFlag (-S option)
Py_UnbufferedStdioFlag (-u, PYTHONUNBUFFEREDIO)
Py_VerboseFlag (-v option, PYTHONVERBOSE)
对于上述变量,命令行选项和环境变量到 C 全局变量的转换由 Py_Main 处理,因此每个嵌入式应用程序都必须相应地设置这些变量才能更改其默认值。
某些配置只能作为操作系统级别的环境变量提供
PYTHONSTARTUP
PYTHONCASEOK
PYTHONIOENCODING
Py_InitializeEx() API 还接受一个布尔标志,用于指示是否安装 CPython 的信号处理器。
最后,某些交互行为(例如打印介绍性横幅)仅在操作系统将标准输入报告为终端连接时触发。
待定:记录如何处理“-x”选项(跳过对主脚本中第一个注释行的处理)
另请参阅 [1] 处详细的操作序列说明。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0432.rst