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,该 API 可用于添加内置扩展模块;先前称为“inittab”的功能。

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

PEP 587“Python 初始化配置”统一了所有配置 Python **初始化** 的方式。此 PEP 还将 Python **预初始化** 和 Python **初始化** 的配置统一到单个 API 中。此外,此 PEP 只提供了一种嵌入 Python 的选择,而不是像 PEP 587 中那样提供两种“Python”和“Isolated”选择,从而进一步简化了 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 成员,而仅向开发分支添加新的 PyConfig.int_max_str_digits 成员(这成为了 Python 3.12)。稳定分支中使用一个专用的私有全局变量(与 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 来在运行时 **设置** 一些配置选项的值

  • optimization_level
  • verbose
  • parser_debug
  • inspect
  • write_bytecode

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

  • 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
inspect 布尔值 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()
交互式 布尔值 API: sys.flags.interactive
模块搜索路径 list[str] API: sys.path
optimization_level int API: sys.flags.optimize
parser_debug 布尔值 API: sys.flags.debug (int)。
platlibdir str API: sys.platlibdir
前缀 str API:sys.base_prefix
pycache_prefix str API: sys.pycache_prefix
静默 布尔值 API: sys.flags.quiet (int)。
stdlib_dir str API: sys._stdlib_dir
使用环境变量 布尔值 API: sys.flags.ignore_environment (int)。
verbose int API: sys.flags.verbose
warnoptions list[str] API: sys.warnoptions
write_bytecode 布尔值 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() 设置。

选项 类型 注释
分配器 int
缓冲的标准 I/O 布尔值
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)。
开发模式 布尔值 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 布尔值
隔离 布尔值 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()
程序名称 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 结尾的 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 结尾的 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 初始化多次,则必须在每次 Python 初始化时调用 PyInitConfig_AddModule()

类似于 PyImport_AppendInittab() 函数。

初始化 Python

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

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

错误处理

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

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

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

错误消息保持有效,直到使用config调用另一个PyInitConfig函数。调用者不必释放错误消息。

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

只有Py_InitializeFromInitConfig()函数可以在parse_argv选项非零的情况下设置退出代码。

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

获取和设置运行时配置

配置选项 name 参数必须是非空、以 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()的config应该在调用该函数之前就已完成,而当前初始化实际上会修改config。

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

区域设置编码和宽字符串

接受编码为区域设置编码的字符串和在PyInitConfig API中接受宽字符串(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