Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 运行时更易于嵌入到更大的应用程序中的背景信息和具体的建议。

在大部分时间里,这些更改要么维护在一个单独的功能分支中,要么作为主 CPython 存储库中带下划线前缀的私有 API。

2019 年,PEP 587 将这些 API 更改中的一个子集迁移到了 Python 3.8+ 的公共 CPython API 中(具体而言,该 PEP 更新了解释器运行时,以提供一个显式多阶段结构化配置接口)。

2020 年 6 月,为了响应指导委员会的一个查询,PEP 作者决定撤回原始 PEP,因为自从 PEP 432 首次编写以来,发生了很多变化,我们认为对启动序列和嵌入 API 的任何进一步更改最好以一个新的 PEP(或多个 PEP)的形式提出,这些 PEP 不仅要考虑 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_MainPy_Initialize API 正式访问,为嵌入式应用程序提供了很少的自定义机会。这种不断增长的复杂性也给维护人员带来了困难,因为许多配置需要在调用 Py_Initialize 之前进行,这意味着无法安全地使用大部分 Python C API。

目前正在讨论一些提案,用于实现更复杂的启动行为,例如,更好地控制 sys.path 初始化(例如,以跨平台的方式轻松地在命令行中添加其他目录 [7],控制 sys.path[0] 的配置 [8]),以及更轻松地配置在启动 Python 子进程时使用的工具(例如,覆盖跟踪 [9])。

与其无限期地将这种行为添加到一个已经很复杂的系统中,该 PEP 提出通过引入一个结构更清晰的启动序列来简化现状,其目的是使这些进一步的功能请求更容易实现。

最初,整个提案都维护在这个 PEP 中,但这被证明是不切实际的,因此,随着拟议的设计部分稳定下来,它们现在被拆分为独立的 PEP,即使总体设计的细节仍在不断发展,也允许取得进展。

关键问题

对启动序列的任何更改都需要考虑几个关键问题。

可维护性

截至 Python 3.6,CPython 启动序列很难理解,更难修改。在执行大部分初始化代码时,解释器处于何种状态并不明确,这会导致诸如列表、字典和 Unicode 值在调用 Py_Initialize 之前创建(当使用 -X-W 选项时 [1])。

通过迁移到一个明确的多阶段启动序列,开发人员只需要了解

  • 在预配置之前哪些 API 和功能可用(实际上没有,除了预配置 API 本身)
  • 在核心运行时配置之前哪些 API 和功能可用,如果预配置尚未显式运行,则将隐式运行具有与 Python 3.6 的行为相匹配的默认设置的预配置。
  • 哪些 API 和功能只有在主解释器完全配置后才能使用(希望这将是完整 C API 的一小部分)

这两个方面的第一个由 PEP 587 涵盖,而后者的细节仍在考虑中。

通过将新设计建立在 C 结构和 Python 数据类型的组合基础上,将来也应该更容易修改系统以添加新的配置选项。

可测试性

CPython 启动序列复杂性的一个问题是不同配置设置之间可能的交互组合爆炸。

这种担忧影响了新初始化系统的设计和提出的实现方法。

性能

CPython 被大量用于运行短脚本,其中运行时间主要由解释器初始化时间决定。对启动序列的任何更改都应尽量减少其对启动开销的影响。

使用 importlib 迁移的经验表明,启动时间主要由 IO 操作决定。但是,为了监控任何更改的影响,可以使用一个简单的基准测试来检查启动解释器并将其拆除需要多长时间

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

(TODO: 使用 perf 而不是 stdlib timeit 运行此微基准测试)

此 PEP 预计不会对启动时间产生任何重大影响,因为它主要针对的是重新排序现有的初始化序列,而不是对各个步骤进行实质性更改。

但是,如果此简单检查表明对初始化序列的拟议更改可能会造成性能问题,那么将开发更复杂的微基准测试来帮助调查。

所需的配置设置

有关 CPython 解释器配置设置的详细列表以及设置这些设置的各种方法,请参阅 PEP 587

实施策略

最初曾尝试在 Python 3.4 [2] 中实现此 PEP 的早期版本,遇到的一个重大问题是在进行了初始结构性更改以开始重构过程后出现了合并冲突。与其他一些之前的重大更改(例如 Python 2.5 中切换到基于 AST 的编译器,或 Python 3.3 中切换到 importlib 实现的导入系统)不同,没有明确的方法来构建不会容易受到影响的草案实现合并冲突类型的冲突,这些冲突困扰着最初的尝试。

因此,实施策略被修改为首先将此重构作为 CPython 3.7 的私有 API 实现,然后审查在 CPython 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 当前状态的头文件主要是

PEP 587 涵盖了被认为可能足够稳定以公开的 API 方面。如果拟议的 API 由该 PEP 涵盖,则会在下面的文本中添加“(参见 PEP 587)”。

此提案的主要主题是在启动过程中的更早阶段初始化核心语言运行时并为主解释器创建一个部分初始化的解释器状态。这将允许在初始化过程的剩余时间中使用大多数 CPython API,这可能会简化目前需要依赖基本 C 功能而不是能够使用 CPython C API 提供的更丰富的数据结构的许多操作。

PEP 587 涵盖了该任务的一个子集,该子集将即使现有“可以在 Py_Initialize 之前调用”的接口也需要的组件(如内存分配器和操作系统接口编码详细信息)分解为一个单独的预配置步骤。

在下文中,“嵌入式应用程序”也涵盖了标准 CPython 命令行应用程序。

解释器初始化阶段

建议以下不同的解释器初始化阶段

  • 未初始化
    • 不完全是一个阶段,而是没有阶段
    • Py_IsInitializing() 返回 0
    • Py_IsRuntimeInitialized() 返回 0
    • Py_IsInitialized() 返回 0
    • 嵌入式应用程序确定使用哪个内存分配器以及使用哪个编码来访问操作系统接口(或选择将这些决定委托给 Python 运行时)
    • 应用程序通过调用其中一个 Py_PreInitialize API 来启动初始化过程(参见 PEP 587
  • 运行时预初始化
    • 没有可用的解释器
    • Py_IsInitializing() 返回 1
    • Py_IsRuntimeInitialized() 返回 0
    • Py_IsInitialized() 返回 0
    • 嵌入式应用程序确定初始化核心 CPython 运行时和创建主解释器所需的设置,并通过调用 Py_InitializeRuntime 进入下一阶段
    • 注意:截至 PEP 587,嵌入式应用程序改为调用 Py_Main()Py_UnixMain 或其中一个 Py_Initialize API,因此直接跳转到已初始化状态。
  • 主解释器初始化
    • 内置数据类型和其他核心运行时服务可用
    • 主解释器可用,但仅部分配置
    • Py_IsInitializing() 返回 1
    • Py_IsRuntimeInitialized() 返回 1
    • Py_IsInitialized() 返回 0
    • 嵌入式应用程序通过调用 Py_InitializeMainInterpreter 确定并应用完成初始化过程所需的设置
    • 注意:截至 PEP 587,此状态无法通过任何公共 API 访问,它只存在于其中一个 Py_Initialize 函数正在运行时的隐式内部状态中
  • 已初始化
    • 主解释器可用且完全运行,但 __main__ 相关的元数据不完整
    • Py_IsInitializing() 返回 0
    • Py_IsRuntimeInitialized() 返回 1
    • Py_IsInitialized() 返回 1

阶段的调用

所有列出的阶段都将被标准 CPython 解释器和提出的系统 Python 解释器使用。

嵌入式应用程序仍然可以通过使用现有的 Py_InitializePy_Main() API 将初始化几乎完全留给 CPython 的控制 - 向后兼容性将得到保留。

或者,如果嵌入式应用程序想要对 CPython 的初始状态有更大的控制权,它将能够使用新的、更细粒度的 API,该 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() 为假,则 Py_InitializeRuntime 函数将隐式调用相应的 Py_PreInitialize 函数。 use_environment 设置将被传递下来,而其他设置将根据其默认值进行处理,如 PEP 587 中所述。

PyInitError 返回类型在 PEP 587 中定义,它允许嵌入式应用程序优雅地处理 Python 运行时初始化失败,而不是让整个进程被 Py_FatalError 突然终止。

新的 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 相关的环境变量的处理。如果该标志为真,则 PYTHONHASHSEED 会正常处理。否则,所有特定于 Python 的环境变量都被视为未定义(对于某些操作系统特定的环境变量,例如在 Mac OS X 上用于在应用程序包和主 Python 二进制文件之间进行通信的环境变量,可能会存在例外情况)。

use_hash_seed 控制随机化哈希算法的配置。如果它为零,则将使用带有随机种子的随机化哈希。如果它是正数,则 hash_seed 中的值将用于播种随机数生成器。如果在这种情况下 hash_seed 为零,则完全禁用随机化哈希。

如果 use_hash_seed 为负数(并且 use_environment 为真),则 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_textNULL、空字符串或值 "random",则 use_hash_seedhash_seed 都将设置为零。否则,use_hash_seed 将设置为 1,并且种子文本将被解释为整数并作为 hash_seed 报告。如果成功,该函数将返回零。非零返回值表示错误(最有可能是在转换为整数时发生错误)。

_install_importlib 设置用作 CPython 构建过程的一部分,以创建一个完全没有导入功能的解释器。它被认为是 CPython 开发团队的私有设置(因此以下划线开头),因为目前唯一支持的用例是允许编译器更改,这些更改使 importlib._bootstrap 的先前冻结字节码失效,而不会破坏构建过程。

目的是使这种初始配置级别尽可能小,以便在不同的嵌入式应用程序中保持引导环境的一致性。如果我们可以在没有设置的情况下创建有效的解释器状态,则该设置应仅出现在全面的 PyConfig 结构体中,而不是核心运行时配置中。

一个新的查询 API 将允许代码确定解释器是否处于核心运行时初始化和创建主解释器状态以及完成主解释器初始化过程的大部分过程之间的引导状态。

int Py_IsRuntimeInitialized();

Py_IsRuntimeInitialized() 已为真时再次尝试调用 Py_InitializeRuntime() 将被报告为用户配置错误。(待办,因为现有的公共初始化 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 调用之前(见下文),这些设置不会应用于正在运行的解释器。

支持的配置设置

解释器配置分为两部分:仅与主解释器相关或必须在主解释器和所有子解释器之间保持一致的设置,以及可能在子解释器之间不同的设置。

注意:出于最初的实现目的,只有指示解释器是否是主解释器的标志将在每个解释器基础上配置。其他字段将针对它们是否可以实现在实现过程中变得特定于解释器进行审查。

注意

下面的配置字段列表目前与 PEP 587 不一致。如果存在差异,则 PEP 587 优先。

PyConfigAsObjects 结构体镜像了来自 PEP 587PyConfig 结构体,但使用完整的 Python 对象来存储值,而不是 C 级数据类型。它添加了 raw_argvargv 列表字段,因此以后的初始化步骤不需要分别接受它们。

字段始终是指向 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 类似,如果在配置数据中发现问题,此调用将引发异常并报告错误返回值,而不是出现致命错误。(待定,因为现有的公共初始化 API 支持多次调用而不会出现错误,并且只是忽略对任何一次性设置的更改。保持这种行为可能比尝试使新 API 比旧 API 更严格更有意义)

所有配置设置都是必需的 - 配置结构体应始终通过 Py_BuildPythonConfig 传递,以确保它已完全填充。

成功调用后,Py_IsInitialized() 将变为 true,而 Py_IsInitializing() 将变为 false。上面描述的仅在核心运行时初始化时解释器阶段的注意事项将不再适用。

Py_IsInitialized() 为 true 时,再次尝试调用 Py_InitializeMainInterpreter() 是一个错误。

但是,与 __main__ 模块相关的某些元数据可能仍然不完整。

  • sys.argv[0] 可能还没有最终值。
    • 在使用 CPython 执行模块或包时,它将是 -m
    • 在执行有效的 sys.path 条目(通常是压缩文件或目录)时,它将与 sys.path[0] 相同,而不是 __main__ 模块的位置。
    • 否则,它将是准确的。
      • 运行普通脚本时,脚本名称。
      • 执行提供的字符串时,为 -c
      • 从标准输入运行时,为 - 或空字符串。
  • __main__ 模块中的元数据仍然表明它是一个内置模块。

此函数通常会隐式地导入 site 作为其最终操作(在 Py_IsInitialized() 已经设置之后)。在配置设置中将“enable_site_config”标志设置为 Py_False 将禁用此行为,并消除在稍后在进程中显式执行 import site 时对全局状态的任何副作用。

准备主模块

注意

PEP 587 中,PyRun_PrepareMainPyRun_ExecMain 没有分别公开,而是通过 Py_RunMain API 访问,该 API 既准备又执行 main,然后完成 Python 解释器的最终化。

此子阶段完成了与 __main__ 模块相关的元数据的填充,而没有真正开始执行 __main__ 模块代码。

它是通过调用以下 API 来处理的。

int PyRun_PrepareMain();

此操作仅允许用于主解释器,并且在从当前线程状态属于子解释器的线程调用时,将引发 RuntimeError

实际处理由存储在解释器状态中的与主相关的设置驱动,这些设置是配置结构体的一部分。

如果 prepare_main 为零,则此调用不执行任何操作。

如果 main_sourcemain_pathmain_modulemain_streammain_code 全部为 NULL,则此调用不执行任何操作。

如果设置了多个 main_sourcemain_pathmain_modulemain_streammain_code,将报告 RuntimeError

如果 main_code 已经设置,则此调用不执行任何操作。

如果设置了 main_stream,并且也设置了 run_implicit_code,则将读取、编译和执行 startup_file 中标识的文件,并在 __main__ 命名空间中执行。

如果设置了 main_sourcemain_pathmain_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__ 添加到 main_module 的末尾并再次尝试(如果最终的名称段已经是 .__main__,则立即失败)。
    • 找到模块源代码后,将编译后的模块代码保存为 main_code,并在 __main__ 中适当地填充以下属性:__name____loader____file____cached____package__

(注意:本节中描述的行为不是新的,它是针对新的配置系统调整的 CPython 解释器当前行为的说明)。

执行主模块

注意

PEP 587 中,PyRun_PrepareMainPyRun_ExecMain 没有分别公开,而是通过 Py_RunMain API 访问,该 API 既准备又执行 main,然后完成 Python 解释器的最终化。

此子阶段涵盖实际 __main__ 模块代码的执行。

它是通过调用以下 API 来处理的。

int PyRun_ExecMain();

此操作仅允许用于主解释器,并且在从当前线程状态属于子解释器的线程调用时,将引发 RuntimeError

实际处理由存储在解释器状态中的与主相关的设置驱动,这些设置是配置结构体的一部分。

如果 main_streammain_code 都是 NULL,则此调用不执行任何操作。

如果 main_streammain_code 都是设置的,则会报告 RuntimeError

如果 main_streamprompt_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__)

配置数据的内部存储

解释器状态将更新为包括初始化期间提供的配置设置的详细信息,方法是通过至少嵌入 PyConfigAsObjectsPyInterpreterConfig 结构体的副本来扩展解释器状态对象。

出于调试目的,配置设置将公开为一个 sys._configuration 简单命名空间(类似于 sys.flagssys.implementation)。这些属性本身将是对应于两个配置设置级别的简单命名空间。

  • all_interpreters
  • active_interpreter

字段名称将与配置结构体中的名称匹配,但 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 解释器比仅仅编写扩展模块涉及更高的耦合程度。

唯一将成为稳定 ABI 部分的新公开 API 是 Py_IsInitializing()Py_IsRuntimeInitialized() 查询。

构建时配置

本 PEP 对构建时配置设置的处理没有进行任何更改,因此不会影响 sys.implementation 的内容或 sysconfig.get_config_vars() 的结果。

向后兼容性

向后兼容性将主要通过确保 Py_BuildPythonConfig() 查询存储在全局变量和环境变量中的所有先前定义的配置设置,以及 Py_InitializeMainInterpreter() 将受影响的设置写回相关位置来维护。

一个公认的不兼容之处是,一些当前延迟读取的环境变量可能会在解释器初始化期间读取一次。随着参考实现的成熟,这些将在逐案的基础上进行更详细的讨论。目前已知会动态查找的环境变量是

  • PYTHONCASEOK:写入 os.environ['PYTHONCASEOK'] 将不再动态改变解释器对导入时文件名大小写差异的处理(待定)
  • PYTHONINSPECTos.environ['PYTHONINSPECT'] 仍然会在 __main__ 模块执行完毕后进行检查

Py_Initialize() 类型的初始化将继续得到支持。它将在内部使用(至少部分)新 API,但将继续表现出与今天相同的行为,确保 sys.argv 不会在后续的 PySys_SetArgv 调用之前填充(待定)。所有当前支持在 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_BuildPythonConfigPy_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 文件。

构建时设置 PREFIXEXEC_PREFIX 也很重要,就像 Windows 上的一些注册表设置一样。硬编码的回退基于在源代码检出中工作时的 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,使相对路径条目看起来像绝对路径。使用 site 模块在最后一种情况下也会导致问题,因为在 3.3 之前的 Python 版本中,显式导入 site 将执行 -S 避免的路径修改,而在 3.3+ 中,将 -m site-S 组合目前会失败)

sys.path[0] 的计算相对简单

  • 对于普通脚本(Python 源代码或编译后的字节码),sys.path[0] 将是包含脚本的目录。
  • 对于有效的 sys.path 条目(通常是压缩文件或目录),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 条目(通常是压缩文件或目录),它最初将是压缩文件或目录名称,但随后会由 runpy 模块更改为导入的 __main__ 模块的完整路径。
  • 对于使用 -m 开关指定的模块,它最初将是字符串 "-m",但随后会由 runpy 模块更改为执行的模块的完整路径。
  • 对于使用 -m 开关指定的包,它最初将是字符串 "-m",但随后会由 runpy 模块更改为执行的包的 __main__ 子模块的完整路径。
  • 对于使用 -c 执行的命令,它将是字符串 "-c"
  • 对于显式请求的来自 stdin 的输入,它将是字符串 "-"
  • 否则,它将为空字符串

嵌入式应用程序必须自己调用 Py_SetArgv。CPython 为此执行的逻辑是 Py_Main() 的一部分,并且没有单独公开。但是,runpy 模块在 runpy.run_modulerunpy.run_path 中提供了大致等效的逻辑。

其他配置设置

待办事项:更详细地介绍以下内容的初始化

  • 完全禁用导入系统
  • 初始警告系统状态
    • sys.warnoptions
    • (-W 选项,PYTHONWARNINGS)
  • 任意扩展选项(例如自动启用 faulthandler
    • sys._xoptions
    • (-X 选项)
  • 由以下使用的文件系统编码
    • sys.getfsencoding
    • os.fsencode
    • os.fsdecode
  • 由以下使用的 IO 编码和缓冲
    • sys.stdin
    • sys.stdout
    • sys.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 处理,因此每个嵌入式应用程序都必须适当地设置它们,以便从默认值更改它们。

某些配置只能作为 OS 级环境变量提供

PYTHONSTARTUP
PYTHONCASEOK
PYTHONIOENCODING

Py_InitializeEx() API 还接受一个布尔标志,以指示是否应安装 CPython 的信号处理程序。

最后,某些交互行为(例如打印介绍性横幅)仅在操作系统报告标准输入为终端连接时触发。

待办事项:记录“ -x”选项的处理方式(跳过处理主脚本中的第一条注释行)

另请参见[1]处的详细操作顺序说明。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0432.rst

最后修改时间:2023-10-11 12:05:51 GMT