PEP 756 – 添加 PyUnicode_Export() 和 PyUnicode_Import() C 函数
- 作者:
- Victor Stinner <vstinner at python.org>
- PEP 代表:
- C API 工作组
- 状态:
- 草案
- 类型:
- 标准追踪
- 创建:
- 2024-09-13
- Python 版本:
- 3.14
摘要
向有限 C API 版本 3.14 添加函数
PyUnicode_Export()
: 将 Python str 对象导出为Py_buffer
视图。PyUnicode_Import()
: 导入 Python str 对象。
一般来说,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
PEP 393 函数,例如 PyUnicode_KIND()
和 PyUnicode_1BYTE_DATA()
,已从有限 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,
int32_t flags,
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 缓冲区。缓冲区的内容在被释放之前有效。
缓冲区是只读的,不得修改。
unicode 和 view 不得为 NULL。
可用格式
常量标识符 | 值 | 描述 |
---|---|---|
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 的未来版本可能会引入其他格式。
导出复杂度
一般来说,导出的复杂度为 O(1): 不需要内存复制。在某些情况下需要复制,复杂度为 O(n)
- 如果仅请求 UCS-2,而原生格式为 UCS-1。
- 如果仅请求 UCS-4,而原生格式为 UCS-1 或 UCS-2。
- 如果仅请求 UTF-8: 字符串在第一次调用时被编码为 UTF-8,然后编码的 UTF-8 字符串被缓存。
为了在 CPython 和 PyPy 上获得 O(1) 的复杂度,建议支持以下 4 种格式
(PyUnicode_FORMAT_UCS1 \
| PyUnicode_FORMAT_UCS2 \
| PyUnicode_FORMAT_UCS4 \
| PyUnicode_FORMAT_UTF8)
Py_buffer 格式和项目大小
Py_buffer
使用以下格式和项目大小,具体取决于导出格式
导出格式 | 缓冲区格式 | 项目大小 |
---|---|---|
PyUnicode_FORMAT_UCS1 |
"B" |
1 字节 |
PyUnicode_FORMAT_UCS2 |
"H" |
2 字节 |
PyUnicode_FORMAT_UCS4 |
"I" 或 "L" |
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 不得为 NULL。nbytes 必须为正数或零。
请参阅 PyUnicode_Export()
以了解可用格式。
UTF-8 格式
CPython 3.14 内部不使用 UTF-8 格式。该格式是为了与 PyPy 兼容而提供的,PyPy 原生使用 UTF-8 表示字符串。然而,在 CPython 中,编码的 UTF-8 字符串被缓存,这使得它方便导出。
在 CPython 上,UTF-8 格式的优先级最低: ASCII 和 UCS 格式优先。
ASCII 格式
当请求 PyUnicode_FORMAT_ASCII
格式进行导出时,将使用 PyUnicode_FORMAT_UCS1
导出格式来表示 ASCII 和 Latin-1 字符串。
PyUnicode_FORMAT_ASCII
格式主要用于 PyUnicode_Import()
,以验证字符串是否只包含 ASCII 字符。
代理字符和 NUL 字符
允许使用代理字符: 可以导入和导出它们。例如,UTF-8 格式使用 surrogatepass
错误处理程序。
允许嵌入式 NUL 字符: 可以导入和导出它们。
导出的字符串不以尾随 NUL 字符结尾: PyUnicode_Export()
调用者必须使用 Py_buffer.len
来获取字符串长度。
实现
向后兼容性
对向后兼容性没有影响,只是添加了新的 C API 函数。
开放问题
- 我们应该保证导出的缓冲区始终以 NUL 字符结尾吗?是否可以在所有 Python 实现中以 O(1) 的复杂度实现它?
- 允许使用代理字符是否可以?
- 我们应该添加一个标志来禁止嵌入式 NUL 字符吗?这将具有 O(n) 的复杂度。
- 我们应该添加一个标志来禁止代理字符吗?这将具有 O(n) 的复杂度。
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 字符查找字符串结尾的代码会截断字符串。这会导致错误,甚至安全漏洞。请参阅问题 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
错误处理程序一起使用来编码和解码代理字符。
讨论
版权
本文件置于公共领域或 CC0-1.0-Universal 许可下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0756.rst
上次修改时间:2024-09-14 09:03:39 GMT