PEP 393 – 灵活的字符串表示
- 作者:
- Martin von Löwis <martin at v.loewis.de>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2010年1月24日
- Python 版本:
- 3.3
- 发布历史:
摘要
Unicode 字符串类型已更改为支持多种内部表示形式,具体取决于具有最大 Unicode 序数(1、2 或 4 字节)的字符。这将允许在常见情况下实现节省空间的表示,同时在所有系统上提供对完整 UCS-4 的访问。为了与现有 API 兼容,可能会并行存在多种表示形式;随着时间的推移,这种兼容性应该逐步淘汰。窄和宽 Unicode 构建之间的区别被取消。此 PEP 的实现可在 [1] 找到。
基本原理
关于当前 unicode 类型实现的抱怨主要有两类:在只支持 UTF-16 的系统上,用户抱怨非 BMP 字符未得到正确支持。在内部使用 UCS-4 的系统(有时也包括使用 UCS-2 的系统)上,抱怨 Unicode 字符串占用过多内存——尤其是与 Python 2.x 相比,在 2.x 中相同的代码通常会使用 ASCII 字符串(即 ASCII 编码的字节字符串)。通过这种提议的方法,纯 ASCII 的 Unicode 字符串将再次只占用每个字符一个字节;同时仍然允许高效地索引包含非 BMP 字符的字符串(因为包含它们的字符串将占用每个字符 4 个字节)。
这种方法的一个问题是支持现有应用程序(例如扩展模块)。为了兼容性,可能会计算冗余表示。鼓励应用程序尽可能逐步淘汰对特定内部表示的依赖。由于与其他库的交互通常需要某种内部表示,因此规范选择 UTF-8 作为向 C 代码公开字符串的推荐方式。
对于许多字符串(例如 ASCII),多种表示形式实际上可能共享内存(例如,如果所有字符都是 ASCII,则最短形式可能与 UTF-8 形式共享)。通过这种共享,兼容性表示的开销得以减少。如果表示形式确实共享数据,还可以省略结构字段,从而减少字符串对象的基准大小。
规范
Unicode 结构现在定义为结构层次结构,即
typedef struct {
PyObject_HEAD
Py_ssize_t length;
Py_hash_t hash;
struct {
unsigned int interned:2;
unsigned int kind:2;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
} state;
wchar_t *wstr;
} PyASCIIObject;
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length;
char *utf8;
Py_ssize_t wstr_length;
} PyCompactUnicodeObject;
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data;
} PyUnicodeObject;
在创建时已知大小和最大字符的对象称为“紧凑”unicode 对象;字符数据紧随基结构之后。如果最大字符小于 128,它们使用 PyASCIIObject 结构,并且 UTF-8 数据、UTF-8 长度和 wstr 长度与 ASCII 数据的长度相同。对于非 ASCII 字符串,使用 PyCompactObject 结构。不支持紧凑对象的调整大小。
在创建时未给出最大字符的对象称为“传统”对象,通过 PyUnicode_FromStringAndSize(NULL, length) 创建。它们使用 PyUnicodeObject 结构。最初,它们的数据只在 wstr 指针中;当调用 PyUnicode_READY 时,数据指针(联合体)被分配。只要尚未调用 PyUnicode_READY,就可以进行调整大小。
字段具有以下解释:
- length:字符串中的码点数量(sq_length 的结果)
- interned:内部化状态 (SSTATE_*),与 3.2 中相同
- kind:字符串形式
- 00 => str 未初始化(数据在 wstr 中)
- 01 => 1 字节 (Latin-1)
- 10 => 2 字节 (UCS-2)
- 11 => 4 字节 (UCS-4);
- compact:对象使用紧凑表示之一(隐含已就绪)
- ascii:对象使用 PyASCIIObject 表示(隐含紧凑且已就绪)
- ready:规范表示已准备好通过 PyUnicode_DATA 和 PyUnicode_GET_LENGTH 访问。如果对象是紧凑的,或者数据指针和长度已初始化,则设置此项。
- wstr_length, wstr: 平台 wchar_t 中的表示(以空字符结尾)。如果 wchar_t 是 16 位,此形式可能使用代理对(在这种情况下,wstr_length 与 length 不同)。wstr_length 仅在表示中存在代理对时才与 length 不同。
- utf8_length, utf8: UTF-8 表示(以空字符结尾)。
- data:Unicode 字符串的最短形式表示。字符串以空字符结尾(在其各自的表示中)。
所有三种表示都是可选的,尽管数据形式被认为是规范表示,它只在字符串创建时可能不存在。如果表示不存在,则指针为 NULL,相应的长度字段可能包含任意数据。
Py_UNICODE 类型仍受支持,但已废弃。它始终定义为 wchar_t 的 typedef,因此 wstr 表示可以兼作 Py_UNICODE 表示。
如果字符串仅使用 ASCII 字符(仅使用 Latin-1 不足),则数据和 utf8 指针指向相同的内存。如果字符串恰好符合平台的 wchar_t 类型(即如果 sizeof(wchar_t) 为 2,则使用一些 BMP 但非 Latin-1 字符;如果 sizeof(wchar_t) 为 4,则使用一些非 BMP 字符),则数据和 wstr 指针指向相同的内存。
字符串创建
创建 Unicode 对象的推荐方法是使用 PyUnicode_New 函数。
PyObject* PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar);
这两个参数都必须表示字符串的最终大小/范围。特别是,使用此 API 的编解码器必须提前计算字符数和最大字符。根据指定的大小和字符范围分配字符串并以 null 结尾;其中的实际字符可能未初始化。
PyUnicode_FromString 和 PyUnicode_FromStringAndSize 仍支持处理 UTF-8 输入;输入被解码,字符串的 UTF-8 表示尚未设置。
PyUnicode_FromUnicode 仍受支持但已废弃。如果 Py_UNICODE 指针非空,则设置数据表示。如果指针为 NULL,则分配一个适当大小的 wstr 表示,可以在调用 PyUnicode_READY()(显式或隐式)之前进行修改。调整 Unicode 字符串的大小在最终确定之前仍然可能。
PyUnicode_READY() 将仅包含 wstr 表示的字符串转换为规范表示。除非 wstr 和 data 可以共享内存,否则 wstr 表示在转换后将被丢弃。此宏在成功时返回 0,在失败时返回 -1,这特别发生在内存分配失败时。
字符串访问
规范表示可以通过两个宏 PyUnicode_Kind 和 PyUnicode_Data 访问。PyUnicode_Kind 返回 PyUnicode_WCHAR_KIND (0)、PyUnicode_1BYTE_KIND (1)、PyUnicode_2BYTE_KIND (2) 或 PyUnicode_4BYTE_KIND (3) 中的一个值。PyUnicode_DATA 返回数据的 void 指针。对单个字符的访问应使用 PyUnicode_{READ|WRITE}[_CHAR]
- PyUnicode_READ(kind, data, index)
- PyUnicode_WRITE(kind, data, index, value)
- PyUnicode_READ_CHAR(unicode, index)
所有这些宏都假定字符串处于规范形式;调用者需要通过调用 PyUnicode_READY 来确保这一点。
提供了一个新函数 PyUnicode_AsUTF8 来访问 UTF-8 表示。它与现有已删除的 _PyUnicode_AsString 相同。此函数在首次调用时会计算 UTF-8 表示。由于此表示将占用内存直到字符串对象释放,因此应用程序应尽可能使用现有 PyUnicode_AsUTF8String(每次都生成一个新的字符串对象)。隐式将字符串转换为 char* 的 API(例如 ParseTuple 函数)将使用 PyUnicode_AsUTF8 来计算转换。
新API
本节总结了 API 的新增功能。
用于访问 Unicode 对象内部表示的宏(只读)
- PyUnicode_IS_COMPACT_ASCII(o), PyUnicode_IS_COMPACT(o), PyUnicode_IS_READY(o)
- PyUnicode_GET_LENGTH(o)
- PyUnicode_KIND(o), PyUnicode_CHARACTER_SIZE(o), PyUnicode_MAX_CHAR_VALUE(o)
- PyUnicode_DATA(o), PyUnicode_1BYTE_DATA(o), PyUnicode_2BYTE_DATA(o), PyUnicode_4BYTE_DATA(o)
字符访问宏
- PyUnicode_READ(kind, data, index), PyUnicode_READ_CHAR(o, index)
- PyUnicode_WRITE(kind, data, index, value)
其他宏
- PyUnicode_READY(o)
- PyUnicode_CONVERT_BYTES(from_type, to_type, begin, end, to)
字符串创建函数
- PyUnicode_New(size, maxchar)
- PyUnicode_FromKindAndData(kind, data, size)
- PyUnicode_Substring(o, start, end)
字符访问工具函数
- PyUnicode_GetLength(o), PyUnicode_ReadChar(o, index), PyUnicode_WriteChar(o, index, character)
- PyUnicode_CopyCharacters(to, to_start, from, from_start, how_many)
- PyUnicode_FindChar(str, ch, start, end, direction)
表示转换
- PyUnicode_AsUCS4(o, buffer, buflen)
- PyUnicode_AsUCS4Copy(o)
- PyUnicode_AsUnicodeAndSize(o, size_out)
- PyUnicode_AsUTF8(o)
- PyUnicode_AsUTF8AndSize(o, size_out)
UCS4 工具函数
- Py_UCS4_{strlen, strcpy, strcat, strncpy, strcmp, strncpy, strcmp, strncmp, strchr, strrchr}
稳定的 ABI
以下函数已添加到稳定 ABI(PEP 384),因为它们独立于 Unicode 对象的实际表示:PyUnicode_New、PyUnicode_Substring、PyUnicode_GetLength、PyUnicode_ReadChar、PyUnicode_WriteChar、PyUnicode_Find、PyUnicode_FindChar。
GDB 调试钩子
Tools/gdb/libpython.py 包含调试钩子,其中嵌入了 CPython 数据类型(包括 PyUnicodeObject 实例)内部的知识。它已更新以跟踪此更改。
废弃、移除和不兼容性
虽然 Py_UNICODE 表示和 API 在本 PEP 中被废弃,但尚未计划移除相应的 API。这些 API 应该在 PEP 被接受后至少五年内保持可用;在它们被移除之前,应该研究现有的扩展模块,以找出 PyPI 上足够多的开源代码是否已移植到新 API。即使在新代码中使用废弃的 API,一个合理的动机是为了使代码在 Python 2 和 Python 3 上都能工作。
以下宏和函数已废弃
- PyUnicode_FromUnicode
- PyUnicode_GET_SIZE, PyUnicode_GetSize, PyUnicode_GET_DATA_SIZE,
- PyUnicode_AS_UNICODE, PyUnicode_AsUnicode, PyUnicode_AsUnicodeAndSize
- PyUnicode_COPY, PyUnicode_FILL, PyUnicode_MATCH
- PyUnicode_Encode, PyUnicode_EncodeUTF7, PyUnicode_EncodeUTF8, PyUnicode_EncodeUTF16, PyUnicode_EncodeUTF32, PyUnicode_EncodeUnicodeEscape, PyUnicode_EncodeRawUnicodeEscape, PyUnicode_EncodeLatin1, PyUnicode_EncodeASCII, PyUnicode_EncodeCharmap, PyUnicode_TranslateCharmap, PyUnicode_EncodeMBCS, PyUnicode_EncodeDecimal, PyUnicode_TransformDecimalToASCII
- Py_UNICODE_{strlen, strcat, strcpy, strcmp, strchr, strrchr}
- PyUnicode_AsUnicodeCopy
- PyUnicode_GetMax
_PyUnicode_AsDefaultEncodedString 已移除。它以前返回一个借用的 UTF-8 编码字节对象的引用。由于 unicode 对象无法再缓存这样的引用,因此在不泄漏内存的情况下实现它是不可能的。没有提供废弃阶段,因为它只是一个内部使用的 API。
使用旧版 API 的扩展模块可能会在无意中调用 PyUnicode_READY,方法是调用一些要求对象已就绪的 API,然后继续访问(现在已失效的)Py_UNICODE 指针。此类代码将因本 PEP 而中断。此代码在 3.2 中就已经存在缺陷,因为没有明确保证 PyUnicode_AS_UNICODE 的结果在 API 调用后会保持有效(由于字符串可能调整大小)。面临此问题的模块需要在 API 调用后重新获取 Py_UNICODE 指针;这样做在早期 Python 版本中将继续正常工作。
讨论
人们对这里提出的方法提出了一些担忧
它使实现更加复杂。这是事实,但考虑到其优点,这是值得的。
Py_UNICODE 表示不是即时可用的,这会降低请求它的应用程序的速度。虽然这也是事实,但关心此问题的应用程序可以重写以使用数据表示。
性能
此补丁的性能必须同时考虑内存消耗和运行时效率。对于内存消耗,预期是拥有许多大字符串的应用程序将看到内存使用量减少。对于小字符串,其效果取决于系统的指针大小和 Py_UNICODE/wchar_t 类型的大小。下表展示了各种小 ASCII 和 Latin-1 字符串大小和平台的情况。
| 字符串大小 | Python 3.2 | 本 PEP | ||||||
| 16位 wchar_t | 32位 wchar_t | ASCII | Latin-1 | |||||
| 32位 | 64位 | 32位 | 64位 | 32位 | 64位 | 32位 | 64位 | |
| 1 | 32 | 64 | 40 | 64 | 32 | 56 | 40 | 80 |
| 2 | 40 | 64 | 40 | 72 | 32 | 56 | 40 | 80 |
| 3 | 40 | 64 | 48 | 72 | 32 | 56 | 40 | 80 |
| 4 | 40 | 72 | 48 | 80 | 32 | 56 | 48 | 80 |
| 5 | 40 | 72 | 56 | 80 | 32 | 56 | 48 | 80 |
| 6 | 48 | 72 | 56 | 88 | 32 | 56 | 48 | 80 |
| 7 | 48 | 72 | 64 | 88 | 32 | 56 | 48 | 80 |
| 8 | 48 | 80 | 64 | 96 | 40 | 64 | 48 | 88 |
运行时效果受所用 API 的显著影响。在将相关代码片段移植到新 API 后,iobench、stringbench 和 json 基准测试通常会出现 1% 到 30% 的减速;对于特定基准测试,可能会出现加速,也可能会出现显著更大的减速。
在对 Django 应用程序的实际测量中([2]),发现了内存使用量显著减少。例如,Unicode 对象的存储空间减少到 2216807 字节,对于宽 Unicode 构建,从 6378540 字节减少,对于窄 Unicode 构建,从 3694694 字节减少(均在 32 位系统上)。这种减少来自于该应用程序中 ASCII 字符串的普遍性;在 36,000 个字符串(包含 1,310,000 个字符)中,有 35713 个是 ASCII 字符串(包含 1,300,000 个字符)。这些字符串的来源未进一步分析;其中许多可能来自库中的标识符,以及 Django 源代码中的字符串常量。
与 Python 2 相比,Unicode 字符串和字节字符串都需要计算在内。在测试应用程序中,Unicode 和字节字符串的总长度在 2.x 中为 2,046,000 单位(字节/字符),在 3.x 中为 2,200,000 单位。在 32 位系统上,其中 2.x 构建使用 32 位 wchar_t/Py_UNICODE,2.x 测试使用 3,620,000 字节,3.x 构建使用 3,340,000 字节。使用此 PEP 的 3.x 版本相对于 2.x 版本的这种减少仅在与宽 Unicode 构建进行比较时发生。
移植指南
只有一小部分 C 代码受到此 PEP 的影响,即需要查看 unicode 字符串“内部”的代码。这些代码不一定需要移植到此 API,因为现有 API 将继续正常工作。特别是,需要同时支持 Python 2 和 Python 3 的模块在同时支持此新 API 和旧 Unicode API 时可能会变得过于复杂。
为了将模块移植到新 API,请尝试消除以下 API 元素的使用:
- Py_UNICODE 类型,
- PyUnicode_AS_UNICODE 和 PyUnicode_AsUnicode,
- PyUnicode_GET_SIZE 和 PyUnicode_GetSize,以及
- PyUnicode_FromUnicode。
遍历现有字符串或查看特定字符时,请使用索引操作而不是指针算术;索引对于 PyUnicode_READ(_CHAR) 和 PyUnicode_WRITE 效果很好。使用 void* 作为字符的缓冲区类型,让编译器检测无效的解引用操作。如果您确实想使用指针算术(例如,在转换现有代码时),请使用 (unsigned) char* 作为缓冲区类型,并将元素大小(1、2 或 4)存储在变量中。请注意,(1<<(kind-1)) 将根据缓冲区类型生成元素大小。
在创建新字符串时,Python 中常见的做法是先使用启发式缓冲区大小,然后根据启发式失败与否进行增长或缩小。有了这个 PEP,这就不那么实用了,因为你不仅需要字符串长度的启发式,还需要最大字符的启发式。
为了避免启发式算法,您需要对输入进行两次遍历:一次确定输出长度和最大字符;然后使用 PyUnicode_New 分配目标字符串,并第二次遍历输入以生成最终输出。虽然这听起来可能很昂贵,但实际上可能比以下方法中再次复制结果更便宜。
如果您采用启发式方法,请避免分配 intended to be resized 的字符串,因为调整字符串大小不适用于它们的规范表示。相反,分配一个单独的缓冲区来收集字符,然后使用 PyUnicode_FromKindAndData 从中构造一个 unicode 对象。一种选择是使用 Py_UCS4 作为缓冲区元素,假设最坏情况的字符序数。这将允许指针算术,但可能需要大量内存。或者,从 1 字节缓冲区开始,并随着遇到更大的字符而增加元素大小。无论如何,PyUnicode_FromKindAndData 将扫描缓冲区以验证最大字符。
对于常见任务,可能不需要直接访问字符串表示:PyUnicode_Find、PyUnicode_FindChar、PyUnicode_Ord 和 PyUnicode_CopyCharacters 有助于分析和创建字符串对象,它们操作的是索引而不是数据指针。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0393.rst
最后修改时间:2025-02-01 08:55:40 GMT