Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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

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

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

unicodeview 不得为 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 来获取字符串长度。

实现

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

向后兼容性

对向后兼容性没有影响,只是添加了新的 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 错误处理程序一起使用来编码和解码代理字符。

讨论


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

上次修改时间:2024-09-14 09:03:39 GMT