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

Python 增强提案

PEP 741 – Python 配置 C API

作者:
Victor Stinner <vstinner at python.org>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
创建日期:
2024 年 1 月 18 日
Python 版本:
3.14
发布历史:
2024 年 1 月 19 日, 2024 年 2 月 8 日
决议:
Discourse 消息

目录

摘要

添加一个 C API,用于配置 Python 初始化,不依赖 C 结构体,并能够在将来进行 ABI 兼容的更改。

通过添加 PyInitConfig_AddModule() 完成 PEP 587 API,该函数可用于添加内置扩展模块;此功能以前称为“inittab”。

添加 PyConfig_Get()PyConfig_Set() 函数,以获取和设置当前运行时配置。

PEP 587 “Python 初始化配置”统一了所有配置 Python 初始化的方式。本 PEP 还将 Python 预初始化和 Python 初始化的配置统一到一个 API 中。此外,本 PEP 只提供一种嵌入 Python 的选择,而不是像 PEP 587 那样提供“Python”和“隔离”两种选择,以进一步简化 API。

更低级别的 PEP 587 PyConfig API 仍然可用于与 CPython 实现细节(例如,模拟 CPython CLI 的全部功能,包括其配置机制)有意更高耦合度的用例。

基本原理

获取运行时配置

PEP 587 没有 API 来获取当前运行时配置,只能配置 Python 初始化

例如,全局配置变量 Py_UnbufferedStdioFlag 在 Python 3.12 中已弃用,建议使用 PyConfig.buffered_stdio 代替。它只用于配置 Python,没有公共 API 来获取 PyConfig.buffered_stdio

有限 C API 的用户正在寻求一个公共 API 来获取当前运行时配置。

Cython 需要获取 optimization_level 配置选项:问题

当全局配置变量在 2022 年弃用时,Marc-André Lemburg 要求一个 C API 来在运行时访问这些配置变量(不仅限于 Python 初始化期间)。

安全修复

为了修复 CVE-2020-10735,一个在将非常大的字符串转换为整数(基数 10)时发生的拒绝服务漏洞,曾讨论过向稳定分支添加一个新的 PyConfig 成员,这会影响 ABI。

Gregory P. Smith 提出了一个不同的 API,使用基于文本的配置文件,以不受 PyConfig 成员的限制:FR: 允许私有运行时配置在不破坏 PyConfig ABI 的情况下进行扩展(2022 年 8 月)。

最终,决定不向稳定分支添加新的 PyConfig 成员,而只向开发分支(后来的 Python 3.12)添加新的 PyConfig.int_max_str_digits 成员。在稳定分支中使用了专用的私有全局变量(与 PyConfig 无关)。

PyPreConfig 和 PyConfig 之间的冗余

Python 预初始化使用 PyPreConfig 结构体,Python 初始化使用 PyConfig 结构体。这两个结构体都有四个重复的成员:dev_modeparse_argvisolateduse_environment

冗余是由于两个结构体分离造成的,而一些 PyConfig 成员是预初始化所需要的。

嵌入 Python

嵌入 Python 的应用程序

示例

在 Linux、FreeBSD 和 macOS 上,应用程序通常要么静态链接到 libpython,要么动态加载 libpythonlibpython 共享库是带版本号的,例如:Linux 上 Python 3.12 的 libpython3.12.so

vim 项目可以针对稳定的 ABI。通常,使用“系统 Python”版本。目前无法选择要使用的 Python 版本。用户希望能够按需选择更新的 Python。

在 Linux 上,部署嵌入 Python 的应用程序(如 GIMP)的另一种方法是,将 Python 包含在 Flatpack、AppImage 或 Snap“容器”中。在这种情况下,应用程序会随容器一起提供其自己的 Python 版本副本。

嵌入 Python 的库

示例

创建独立应用程序的实用程序

这些实用程序创建独立应用程序,它们不链接到 libpython。

设置运行时配置

Marc-André Lemburg 请求一个 C API 来在运行时设置某些配置选项的值

  • 优化级别
  • 详细
  • 解析器调试
  • 检查
  • 写入字节码

以前,可以直接设置全局配置变量

  • Py_OptimizeFlag
  • Py_VerboseFlag
  • Py_DebugFlag
  • Py_InspectFlag
  • Py_DontWriteBytecodeFlag

但这些配置标志在 Python 3.12 中已弃用,并计划在 Python 3.14 中移除。

规范

添加 C API 函数和结构体以配置 Python 初始化

  • 创建配置
    • PyInitConfig 不透明结构体。
    • PyInitConfig_Create().
    • PyInitConfig_Free(config).
  • 获取选项
    • PyInitConfig_HasOption(config, name).
    • PyInitConfig_GetInt(config, name, &value).
    • PyInitConfig_GetStr(config, name, &value).
    • PyInitConfig_GetStrList(config, name, &length, &items).
    • PyInitConfig_FreeStrList().
  • 设置选项
    • PyInitConfig_SetInt(config, name, value).
    • PyInitConfig_SetStr(config, name, value).
    • PyInitConfig_SetStrList(config, name, length, items).
    • PyInitConfig_AddModule(config, name, initfunc)
  • 初始化
    • Py_InitializeFromInitConfig(config).
  • 错误处理
    • PyInitConfig_GetError(config, &err_msg).
    • PyInitConfig_GetExitcode(config, &exitcode).

添加 C API 函数以获取和设置当前运行时配置

  • PyConfig_Get(name).
  • PyConfig_GetInt(name, &value).
  • PyConfig_Set(name).
  • PyConfig_Names().

C API 使用以 null 结尾的 UTF-8 编码字符串来引用配置选项名称。

这些 C API 函数不包括在有限 C API 中。

PyInitConfig 结构体

PyInitConfig 结构体通过组合 PyConfig API 的三个结构体来实现,并且还包含一个 inittab 成员。

  • PyPreConfig preconfig
  • PyConfig config
  • PyStatus status
  • struct _inittab *inittab 用于 PyInitConfig_AddModule()

PyStatus 状态不再分离,而是统一的 PyInitConfig 结构体的一部分,这使得 API 更易于使用。

配置选项

配置选项以 PyPreConfigPyConfig 结构体成员命名。请参阅 PyPreConfig 文档PyConfig 文档

弃用和移除配置选项超出本 PEP 的范围,应根据具体情况进行讨论。

公共配置选项

以下选项可以通过 PyConfig_Get() 获取,并通过 PyConfig_Set() 设置。

选项 类型 注释
argv list[str] API:sys.argv
base_exec_prefix str API:sys.base_exec_prefix
base_executable str API:sys._base_executable
base_prefix str API:sys.base_prefix
bytes_warning int API:sys.flags.bytes_warning
exec_prefix str API:sys.exec_prefix
executable str API:sys.executable
检查 布尔值 API:sys.flags.inspect (int)。
int_max_str_digits int API:sys.flags.int_max_str_digitssys.get_int_max_str_digits()sys.set_int_max_str_digits()
interactive 布尔值 API:sys.flags.interactive
module_search_paths list[str] API:sys.path
优化级别 int API:sys.flags.optimize
解析器调试 布尔值 API:sys.flags.debug (int)。
platlibdir str API:sys.platlibdir
prefix str API:sys.base_prefix
pycache_prefix str API:sys.pycache_prefix
quiet 布尔值 API:sys.flags.quiet (int)。
stdlib_dir str API:sys._stdlib_dir
use_environment 布尔值 API:sys.flags.ignore_environment (int)。
详细 int API:sys.flags.verbose
warnoptions list[str] API:sys.warnoptions
写入字节码 布尔值 API:sys.flags.dont_write_bytecode (int) 和 sys.dont_write_bytecode (bool)。
xoptions dict[str, str] API:sys._xoptions

一些选项名称与 sys 属性不同,例如 optimization_level 选项和 sys.flags.optimize 属性。PyConfig_Set() 设置相应的 sys 属性。

xoptionsPyInitConfig 中是一个字符串列表,每个字符串的格式为 keyvalue 隐式为 True)或 key=value。在当前运行时配置中,它会变成一个字典(key: strvalue: str | True)。

只读配置选项

以下选项可以通过 PyConfig_Get() 获取,但不能通过 PyConfig_Set() 设置。

选项 类型 注释
allocator int
buffered_stdio 布尔值
check_hash_pycs_mode str
code_debug_ranges 布尔值
coerce_c_locale 布尔值
coerce_c_locale_warn 布尔值
configure_c_stdio 布尔值
configure_locale 布尔值
cpu_count int API:os.cpu_count() (int | None)。
dev_mode 布尔值 API:sys.flags.dev_mode
dump_refs 布尔值
dump_refs_file str
faulthandler 布尔值 API:faulthandler.is_enabled()
filesystem_encoding str API:sys.getfilesystemencoding()
filesystem_errors str API:sys.getfilesystemencodeerrors()
hash_seed int
home str
import_time 布尔值
install_signal_handlers 布尔值
isolated 布尔值 API:sys.flags.isolated (int)。
legacy_windows_fs_encoding 布尔值 仅限 Windows。
legacy_windows_stdio 布尔值 仅限 Windows。
malloc_stats 布尔值
orig_argv list[str] API:sys.orig_argv
parse_argv 布尔值
pathconfig_warnings 布尔值
perf_profiling 布尔值 API:sys.is_stack_trampoline_active()
program_name str
run_command str
run_filename str
run_module str
run_presite str 需要调试构建。
safe_path 布尔值
show_ref_count 布尔值
site_import 布尔值 API:sys.flags.no_site (int)。
skip_source_first_line 布尔值
stdio_encoding str API:sys.stdin.encodingsys.stdout.encodingsys.stderr.encoding
stdio_errors str API:sys.stdin.errorssys.stdout.errorssys.stderr.errors
tracemalloc int API:tracemalloc.is_tracing() (bool)。
use_frozen_modules 布尔值
use_hash_seed 布尔值
user_site_directory 布尔值 API:sys.flags.no_user_site (int)。
utf8_mode 布尔值
warn_default_encoding 布尔值
_pystats 布尔值 API:sys._stats_on()sys._stats_off()。需要 Py_STATS 构建。

创建配置

PyInitConfig 结构体
用于配置 Python 预初始化和 Python 初始化的不透明结构体。
PyInitConfig* PyInitConfig_Create(void):
使用 隔离配置 的默认值创建一个新的初始化配置。

必须使用 PyInitConfig_Free() 释放。

内存分配失败时返回 NULL

void PyInitConfig_Free(PyInitConfig *config):
释放初始化配置的内存。

获取选项

配置选项 name 参数必须是一个非 NULL 的以 null 结尾的 UTF-8 编码字符串。

int PyInitConfig_HasOption(PyInitConfig *config, const char *name):
测试配置是否有一个名为 name 的选项。

如果选项存在,则返回 1,否则返回 0

int PyInitConfig_GetInt(PyInitConfig *config, const char *name, int64_t *value):
获取整数配置选项。
  • 设置 *value,成功时返回 0
  • config 中设置错误并返回 -1
int PyInitConfig_GetStr(PyInitConfig *config, const char *name, char **value):
获取字符串配置选项,作为以 null 结尾的 UTF-8 编码字符串。
  • 设置 *value,成功时返回 0
  • config 中设置错误并返回 -1

成功时,字符串必须使用 free(value) 释放。

int PyInitConfig_GetStrList(PyInitConfig *config, const char *name, size_t *length, char ***items):
获取字符串列表配置选项,作为以 null 结尾的 UTF-8 编码字符串数组。
  • 设置 *length*value,成功时返回 0
  • config 中设置错误并返回 -1

成功时,字符串列表必须使用 PyInitConfig_FreeStrList(length, items) 释放。

void PyInitConfig_FreeStrList(size_t length, char **items):
释放由 PyInitConfig_GetStrList() 创建的字符串列表的内存。

设置选项

配置选项 name 参数必须是一个非 NULL 的以 null 结尾的 UTF-8 编码字符串。

某些配置选项对其他选项有副作用。此逻辑仅在调用 Py_InitializeFromInitConfig() 时实现,而不是由下面的“Set”函数实现。例如,将 dev_mode 设置为 1 不会将 faulthandler 设置为 1

int PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value):
设置整数配置选项。
  • 成功时返回 0
  • config 中设置错误并返回 -1
int PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char *value):
从以 null 结尾的 UTF-8 编码字符串设置字符串配置选项。字符串被复制。
  • 成功时返回 0
  • config 中设置错误并返回 -1
int PyInitConfig_SetStrList(PyInitConfig *config, const char *name, size_t length, char * const *items):
从以 null 结尾的 UTF-8 编码字符串数组设置字符串列表配置选项。字符串列表被复制。
  • 成功时返回 0
  • config 中设置错误并返回 -1
int PyInitConfig_AddModule(PyInitConfig *config, const char *name, PyObject* (*initfunc)(void)):
向内置模块表添加一个内置扩展模块。

新模块可以通过名称 name 导入,并使用函数 initfunc 作为首次尝试导入时调用的初始化函数。

  • 成功时返回 0
  • config 中设置错误并返回 -1

如果 Python 被初始化多次,PyInitConfig_AddModule() 必须在每次 Python 初始化时调用。

类似于 PyImport_AppendInittab() 函数。

初始化 Python

int Py_InitializeFromInitConfig(PyInitConfig *config):
从初始化配置初始化 Python。
  • 成功时返回 0
  • config 中设置错误并返回 -1
  • 如果 Python 希望退出,则在 config 中设置退出代码并返回 -1

有关退出代码的情况,请参阅 PyInitConfig_GetExitcode()

错误处理

int PyInitConfig_GetError(PyInitConfig* config, const char **err_msg):
获取 config 错误消息。
  • 如果设置了错误,则设置 *err_msg 并返回 1
  • 否则,将 *err_msg 设置为 NULL 并返回 0

错误消息是 UTF-8 编码字符串。

如果 config 有退出代码,则将退出代码格式化为错误消息。

错误消息在调用另一个 PyInitConfig 函数并传入 config 之前保持有效。调用者不必释放错误消息。

int PyInitConfig_GetExitcode(PyInitConfig* config, int *exitcode):
获取 config 退出代码。
  • 如果 Python 希望退出,则设置 *exitcode 并返回 1
  • 如果 config 没有设置退出代码,则返回 0

只有在 parse_argv 选项非零时,Py_InitializeFromInitConfig() 函数才能设置退出代码。

当解析命令行失败(退出代码 2)或命令行选项要求显示命令行帮助(退出代码 0)时,可以设置退出代码。

获取和设置运行时配置

配置选项 name 参数必须是一个非 NULL 的以 null 结尾的 UTF-8 编码字符串。

PyObject* PyConfig_Get(const char *name):
获取配置选项的当前运行时值作为 Python 对象。
  • 成功时返回一个新引用。
  • 设置异常并在出错时返回 NULL

对象类型取决于选项:请参阅 配置选项 表。

其他选项从内部 PyPreConfigPyConfig 结构体中获取。

调用者必须持有 GIL。该函数不能在 Python 初始化之前或 Python 终结之后调用。

int PyConfig_GetInt(const char *name, int *value):
类似于 PyConfig_Get(),但将值作为整数获取。
  • 设置 *value 并成功时返回 0
  • 设置异常并在出错时返回 -1
PyObject* PyConfig_Names(void):
获取所有配置选项名称作为 frozenset

设置异常并在出错时返回 NULL

调用者必须持有 GIL。

PyObject* PyConfig_Set(const char *name, PyObject *value):
设置配置选项的当前运行时值。
  • 如果没有选项 name,则引发 ValueError
  • 如果 value 是无效值,则引发 ValueError
  • 如果选项是只读的(不能设置),则引发 ValueError
  • 如果 value 类型不正确,则引发 TypeError

只读配置选项 不能设置。

调用者必须持有 GIL。该函数不能在 Python 初始化之前或 Python 终结之后调用。

稳定性

选项的行为、默认选项值和 Python 行为在每个 Python 版本中都可能发生变化:它们不是“稳定的”。

此外,配置选项的添加、弃用和移除遵循通常的 PEP 387 弃用流程。

与 PyPreConfig 和 PyConfig API 的交互

更低级别的 PEP 587 PyPreConfigPyConfig API 仍然可用并得到完全支持。正如摘要中所述,对于那些旨在密切模拟完整 CPython CLI 行为(而不仅仅是将 Python 运行时作为更大应用程序的一部分提供)的嵌入用例,它们仍然是首选方法。

PyPreConfig API 可以与本 PEP 中的初始化 API 结合使用。在这种情况下,预配置设置的只读与读/写限制适用于 PyInitConfig_SetInt 以及 PyConfig_Set(一旦解释器已被预配置)(具体来说,只有 use_environment 可以更新,尝试更新任何其他预配置变量将报告错误)。

示例

初始化 Python

示例:初始化 Python,设置各种类型的配置选项,出错时返回 -1

int init_python(void)
{
    PyInitConfig *config = PyInitConfig_Create();
    if (config == NULL) {
        printf("PYTHON INIT ERROR: memory allocation failed\n");
        return -1;
    }

    // Set an integer (dev mode)
    if (PyInitConfig_SetInt(config, "dev_mode", 1) < 0) {
        goto error;
    }

    // Set a list of UTF-8 strings (argv)
    char *argv[] = {"my_program", "-c", "pass"};
    if (PyInitConfig_SetStrList(config, "argv",
                                 Py_ARRAY_LENGTH(argv), argv) < 0) {
        goto error;
    }

    // Set a UTF-8 string (program name)
    if (PyInitConfig_SetStr(config, "program_name", L"my_program") < 0) {
        goto error;
    }

    // Initialize Python with the configuration
    if (Py_InitializeFromInitConfig(config) < 0) {
        goto error;
    }
    PyInitConfig_Free(config);
    return 0;

error:
    // Display the error message
    const char *err_msg;
    (void)PyInitConfig_GetError(config, &err_msg);
    printf("PYTHON INIT ERROR: %s\n", err_msg);
    PyInitConfig_Free(config);

    return -1;
}

增加初始化 bytes_warning 选项

示例:增加初始化配置的 bytes_warning 选项

int config_bytes_warning(PyInitConfig *config)
{
    int64_t bytes_warning;
    if (PyInitConfig_GetInt(config, "bytes_warning", &bytes_warning)) {
        return -1;
    }
    bytes_warning += 1;
    if (PyInitConfig_SetInt(config, "bytes_warning", bytes_warning)) {
        return -1;
    }
    return 0;
}

获取运行时 verbose 选项

示例:获取配置选项 verbose 的当前运行时值

int get_verbose(void)
{
    int verbose;
    if (PyConfig_GetInt("verbose", &verbose) < 0) {
        // Silently ignore the error
        PyErr_Clear();
        return -1;
    }
    return verbose;
}

出错时,函数会静默忽略错误并返回 -1。实际上,获取 verbose 选项不会失败,除非未来的 Python 版本移除了该选项。

实施

向后兼容性

更改完全向后兼容。只添加了新的 API。

现有 API,例如 PyConfig C API (PEP 587) 保持不变。

被拒绝的想法

文本形式的配置

曾有人提议以文本形式提供配置,以使 API 与稳定的 ABI 兼容,并允许自定义选项。

示例

# integer
bytes_warning = 2

# string
filesystem_encoding = "utf8"   # comment

# list of strings
argv = ['python', '-c', 'code']

该 API 会将配置作为字符串而不是文件。例如,使用假想的 PyInit_SetConfig() 函数

void stable_abi_init_demo(int set_path)
{
    PyInit_SetConfig(
        "isolated = 1\n"
        "argv = ['python', '-c', 'code']\n"
        "filesystem_encoding = 'utf-8'\n"
    );
    if (set_path) {
        PyInit_SetConfig("pythonpath = '/my/path'");
    }
}

该示例忽略了错误处理,以方便阅读。

问题是,生成这样的配置文本需要给字符串添加引号并转义字符串中的引号。格式化字符串数组变得不那么简单。

提供一个 API 来格式化字符串或字符串数组并不值得,而 Python 可以直接提供一个 API 来设置配置选项,其中值直接作为字符串或字符串数组传递。这避免了赋予某些字符(例如换行符)特殊含义,这些字符将不得不被转义。

通过整数引用选项

使用字符串来引用配置选项需要比较字符串,这可能比比较整数慢。

使用整数(类似于类型“槽”,例如 Py_tp_doc)来引用配置选项。const char *name 参数替换为 int option

在使用整数时,接受自定义选项更容易导致冲突,因为更难维护整数选项的“命名空间”(范围)。使用字符串时,可以使用带有冒号分隔符的简单前缀。

整数还需要维护一个整数常量列表,这会使 C API 和 Python API 更大。

Python 3.13 只有大约 62 个配置选项,因此性能并不是真正的瓶颈问题。如果将来需要更好的性能,可以使用哈希表通过名称获取选项。

如果在热代码中使用获取配置选项,则可以读取并缓存一次该值。顺便说一下,大多数配置选项不能在运行时更改。

多阶段初始化(类似于 PEP 432)

Eric Snow 表达了担忧,认为这项提案可能会向嵌入者强化初始化是一个单一整体步骤的观念。他认为初始化涉及 5 个不同的阶段,甚至建议 API 应该明确反映这一点。Eric 提议,至少,初始化的实现应该反映这些阶段,部分是为了改善代码健康。总的来说,他的解释与 PEP 432PEP 587 有些相似之处。

Eric 的另一个与本 PEP 相关的重要观点是,理想情况下,传递给 Py_InitializeFromConfig() 的配置应该在该函数被调用之前完成,而目前初始化实际上会修改配置。

虽然 Eric 不一定是在提出 PEP 741 的替代方案,但任何围绕阶段添加细粒度初始化 API 的提议实际上与本 PEP 试图实现的目标相反。这样的 API 更复杂,它需要添加新的公共结构体和新的公共函数。它使 Python 初始化更加复杂,而不是本 PEP 试图统一现有 API 并使其更简单(相反)。为相似目的使用多个结构体可能会导致成员重复,类似于现有 PyPreConfigPyConfig 结构体之间重复成员的问题。

区域设置编码和宽字符串

PyInitConfig API 中接受编码为 locale 编码的字符串和接受宽字符串 (wchar_t*) 的提议被推迟,以保持 PyInitConfig API 的简单性,并避免 Python 预初始化的复杂性。这些功能也主要在模拟完整的 CPython CLI 行为时才需要,因此更适合由更低级别的 PEP 587 API 提供服务。

讨论


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

最后修改:2024-09-03 13:37:25 GMT