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

Python 增强提案

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)。
  • 出错时,设置异常,并返回 -1view保持不变。

成功调用 PyUnicode_Export() 后,必须使用 PyBuffer_Release() 释放view缓冲区。缓冲区的内容在释放之前有效。

缓冲区是只读的,不得修改。

必须使用view->len成员来获取字符串长度。缓冲区应以尾随的 NUL 字符结尾,但不建议依赖此行为,因为存在嵌入的 NUL 字符。

unicodeview不能为空。

可用格式

常量标识符 描述
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 字符:它们可以被导入和导出。

实施

https://github.com/python/cpython/pull/123738

向后兼容性

对向后兼容性没有影响,仅添加了新的 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 的想法被放弃了。

讨论


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

最后修改:2025-02-01 07:28:42 GMT