PEP 756 – 添加 PyUnicode_Export() 和 PyUnicode_Import() C 函数
- 作者:
- Victor Stinner <vstinner at python.org>
- PEP 代理人:
- C API 工作组
- 讨论至:
- Discourse 帖子
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建日期:
- 2024年9月13日
- Python 版本:
- 3.14
- 发布历史:
- 2024年9月14日
- 决议:
- 2024年10月29日
摘要
向有限 C API 版本 3.14 添加函数
PyUnicode_Export():将 Python str 对象导出为Py_buffer视图。PyUnicode_Import():导入 Python str 对象。
在 CPython 上,PyUnicode_Export() 的复杂度为O(1):不复制内存,也不进行转换。
基本原理
PEP 393
PEP 393 “灵活的字符串表示” 在 Python 3.3 中改变了字符串的内部结构,使其使用三种格式
PyUnicode_1BYTE_KIND:Unicode 范围 [U+0000; U+00ff],UCS-1,每字符 1 字节。PyUnicode_2BYTE_KIND:Unicode 范围 [U+0000; U+ffff],UCS-2,每字符 2 字节。PyUnicode_4BYTE_KIND:Unicode 范围 [U+0000; U+10ffff],UCS-4,每字符 4 字节。
Python str 对象必须始终使用最紧凑的格式。例如,仅包含 ASCII 字符的字符串必须使用 UCS-1 格式。
可以使用 PyUnicode_KIND() 函数来了解字符串使用的格式。
可以使用以下函数之一来访问数据
PyUnicode_1BYTE_DATA()用于PyUnicode_1BYTE_KIND。PyUnicode_2BYTE_DATA()用于PyUnicode_2BYTE_KIND。PyUnicode_4BYTE_DATA()用于PyUnicode_4BYTE_KIND。
为了获得最佳性能,C 扩展应为这 3 种字符串原生格式中的每一种提供 3 个代码路径。
有限 C API
像 PyUnicode_KIND() 和 PyUnicode_1BYTE_DATA() 这样的 PEP 393 函数不包含在有限 C API 中。无法编写针对 UCS 格式进行优化的代码。使用有限 C API 的 C 扩展只能使用效率较低的代码路径和字符串格式。
例如,MarkupSafe 项目拥有一个针对 UCS 格式进行优化的 C 扩展,以获得最佳性能,因此无法使用有限 C API。
规范
API
向有限 C API 版本 3.14 添加以下 API
int32_t PyUnicode_Export(
PyObject *unicode,
int32_t requested_formats,
Py_buffer *view);
PyObject* PyUnicode_Import(
const void *data,
Py_ssize_t nbytes,
int32_t format);
#define PyUnicode_FORMAT_UCS1 0x01 // Py_UCS1*
#define PyUnicode_FORMAT_UCS2 0x02 // Py_UCS2*
#define PyUnicode_FORMAT_UCS4 0x04 // Py_UCS4*
#define PyUnicode_FORMAT_UTF8 0x08 // char*
#define PyUnicode_FORMAT_ASCII 0x10 // char* (ASCII string)
使用 int32_t 类型而不是 int,以确保明确的类型大小,并且不依赖于平台或编译器。有关更详细的理由,请参见 避免使用 C 特定类型。
PyUnicode_Export()
API
int32_t PyUnicode_Export(
PyObject *unicode,
int32_t requested_formats,
Py_buffer *view)
以 requested_formats 指定的格式之一导出unicode字符串的内容。
- 成功时,填充view,并返回一个格式(大于
0)。 - 出错时,设置异常,并返回
-1。view保持不变。
成功调用 PyUnicode_Export() 后,必须使用 PyBuffer_Release() 释放view缓冲区。缓冲区的内容在释放之前有效。
缓冲区是只读的,不得修改。
必须使用view->len成员来获取字符串长度。缓冲区应以尾随的 NUL 字符结尾,但不建议依赖此行为,因为存在嵌入的 NUL 字符。
unicode和view不能为空。
可用格式
| 常量标识符 | 值 | 描述 |
|---|---|---|
PyUnicode_FORMAT_UCS1 |
0x01 |
UCS-1 字符串 (Py_UCS1*) |
PyUnicode_FORMAT_UCS2 |
0x02 |
UCS-2 字符串 (Py_UCS2*) |
PyUnicode_FORMAT_UCS4 |
0x04 |
UCS-4 字符串 (Py_UCS4*) |
PyUnicode_FORMAT_UTF8 |
0x08 |
UTF-8 字符串 (char*) |
PyUnicode_FORMAT_ASCII |
0x10 |
ASCII 字符串 (Py_UCS1*) |
UCS-2 和 UCS-4 使用本机字节序。
requested_formats可以是单个格式,也可以是表中格式的按位组合。成功时,返回的格式将设置为其中一种请求的格式。
请注意,未来的 Python 版本可能会引入其他格式。
不复制内存,也不进行转换。
导出复杂性
在 CPython 上,导出的复杂度为O(1):不复制内存,也不进行转换。
为了在 CPython 和 PyPy 上获得最佳性能,建议支持这 4 种格式
(PyUnicode_FORMAT_UCS1 \
| PyUnicode_FORMAT_UCS2 \
| PyUnicode_FORMAT_UCS4 \
| PyUnicode_FORMAT_UTF8)
PyPy 原生使用 UTF-8,因此推荐使用 PyUnicode_FORMAT_UTF8 格式。这需要内存复制,因为 PyPy str 对象可能会在内存中移动(PyPy 使用垃圾回收器)。
Py_buffer 格式和项大小
Py_buffer 根据导出格式使用以下格式和项大小
| 导出格式 | 缓冲区格式 | 项大小 |
|---|---|---|
PyUnicode_FORMAT_UCS1 |
"B" |
1 字节 |
PyUnicode_FORMAT_UCS2 |
"=H" |
2 字节 |
PyUnicode_FORMAT_UCS4 |
"=I" |
4 字节 |
PyUnicode_FORMAT_UTF8 |
"B" |
1 字节 |
PyUnicode_FORMAT_ASCII |
"B" |
1 字节 |
PyUnicode_Import()
API
PyObject* PyUnicode_Import(
const void *data,
Py_ssize_t nbytes,
int32_t format)
从支持格式的缓冲区创建 Unicode 字符串对象。
- 成功时返回指向新字符串对象的引用。
- 出错时设置异常并返回
NULL。
data不能为空。nbytes必须大于或等于零。
有关可用格式,请参见 PyUnicode_Export()。
UTF-8 格式
CPython 3.14 不在内部使用 UTF-8 格式,也不支持将字符串导出为 UTF-8。可以使用 PyUnicode_AsUTF8AndSize() 函数代替。
提供 PyUnicode_FORMAT_UTF8 格式是为了与可能原生使用 UTF-8 进行字符串处理的替代实现兼容。
ASCII 格式
当请求 PyUnicode_FORMAT_ASCII 格式进行导出时,对于 ASCII 字符串,将使用 PyUnicode_FORMAT_UCS1 导出格式。
PyUnicode_FORMAT_ASCII 格式主要用于 PyUnicode_Import() 以验证字符串仅包含 ASCII 字符。
代理字符和嵌入的 NUL 字符
允许代理字符:它们可以被导入和导出。
允许嵌入的 NUL 字符:它们可以被导入和导出。
实施
向后兼容性
对向后兼容性没有影响,仅添加了新的 C API 函数。
PEP 393 C API 的用法
对 PyPI 前 7,500 个项目(2024 年 3 月)的代码搜索显示,有许多项目使用常规 C API 导入和导出 UCS 格式。
PyUnicode_FromKindAndData()
25 个项目调用 PyUnicode_FromKindAndData()
- Cython (3.0.9)
- Levenshtein (0.25.0)
- PyICU (2.12)
- PyICU-binary (2.7.4)
- PyQt5 (5.15.10)
- PyQt6 (6.6.1)
- aiocsv (1.3.1)
- asyncpg (0.29.0)
- biopython (1.83)
- catboost (1.2.3)
- cffi (1.16.0)
- mojimoji (0.0.13)
- mwparserfromhell (0.6.6)
- numba (0.59.0)
- numpy (1.26.4)
- orjson (3.9.15)
- pemja (0.4.1)
- pyahocorasick (2.0.0)
- pyjson5 (1.6.6)
- rapidfuzz (3.6.2)
- regex (2023.12.25)
- srsly (2.4.8)
- tokenizers (0.15.2)
- ujson (5.9.0)
- unicodedata2 (15.1.0)
PyUnicode_4BYTE_DATA()
21 个项目调用 PyUnicode_2BYTE_DATA() 和/或 PyUnicode_4BYTE_DATA()
- Cython (3.0.9)
- MarkupSafe (2.1.5)
- Nuitka (2.1.2)
- PyICU (2.12)
- PyICU-binary (2.7.4)
- PyQt5_sip (12.13.0)
- PyQt6_sip (13.6.0)
- biopython (1.83)
- catboost (1.2.3)
- cement (3.0.10)
- cffi (1.16.0)
- duckdb (0.10.0)
- mypy (1.9.0)
- numpy (1.26.4)
- orjson (3.9.15)
- pemja (0.4.1)
- pyahocorasick (2.0.0)
- pyjson5 (1.6.6)
- pyobjc-core (10.2)
- sip (6.8.3)
- wxPython (4.2.1)
被拒绝的想法
拒绝嵌入的 NUL 字符并要求尾随 NUL 字符
在 C 语言中,方便使用尾随的 NUL 字符。例如,可以使用 for (; *str != 0; str++) 循环遍历字符,并使用 strlen() 获取字符串长度。
问题是 Python str 对象可以嵌入 NUL 字符。例如:"ab\0c"。如果字符串包含嵌入的 NUL 字符,依赖 NUL 字符查找字符串结尾的代码会截断字符串。这可能导致 bug,甚至安全漏洞。请参阅之前在问题 Change PyUnicode_AsUTF8() to return NULL on embedded null characters 中的讨论。
拒绝嵌入的 NUL 字符需要扫描字符串,这具有O(n) 的复杂度。
拒绝代理字符
代理字符是 Unicode 范围 [U+D800; U+DFFF] 中的字符。它们被 UTF 编解码器(如 UTF-8)禁止。Python str 对象可以包含任意的孤立代理字符。例如:"\uDC80"。
拒绝代理字符会阻止导出包含此类字符的字符串。这可能令人惊讶且烦恼,因为 PyUnicode_Export() 的调用者无法控制字符串内容。
允许代理字符可以导出任何字符串,从而避免此问题。例如,UTF-8 编解码器可以与 surrogatepass 错误处理程序一起使用来编码和解码代理字符。
按需转换
按需转换格式会很方便。例如,如果请求仅导出为 UCS-4,则将 UCS-1 和 UCS-2 转换为 UCS-4。
问题在于大多数用户期望导出不需要内存复制和转换:复杂度为O(1)。最好有一个 API,其中所有操作的复杂度都为O(1)。
导出为 UTF-8
CPython 3.14 有一个缓存用于将字符串编码为 UTF-8。允许导出为 UTF-8 具有诱惑力。
问题在于 UTF-8 缓存不支持代理字符。导出操作应提供完整的字符串内容,包括嵌入的 NUL 字符和代理字符。为了导出代理字符,需要一个不同的代码路径,使用 surrogatepass 错误处理程序,并且每次导出操作都必须分配一个临时缓冲区:复杂度为O(n)。
导出操作预计复杂度为O(1),因此在 CPython 中导出 UTF-8 的想法被放弃了。
讨论
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0756.rst