PEP 393 – 字符串灵活表示
- 作者:
- Martin von Löwis <martin at v.loewis.de>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2010-01-24
- Python 版本:
- 3.3
- 历史记录:
摘要
Unicode 字符串类型已更改为支持多种内部表示,具体取决于字符的最大 Unicode 序数(1、2 或 4 字节)。这将允许在常见情况下使用空间高效的表示,但在所有系统上都能访问完整的 UCS-4。为了与现有 API 兼容,可以并行存在多种表示;随着时间的推移,应逐步淘汰这种兼容性。窄 Unicode 构建和宽 Unicode 构建之间的区别已删除。此 PEP 的实现可在 [1] 中找到。
基本原理
关于 unicode 类型的当前实现,有两类抱怨:在仅支持 UTF-16 的系统上,用户抱怨非 BMP 字符不受正确支持。在内部使用 UCS-4 的系统上(有时还在使用 UCS-2 的系统上),人们抱怨 Unicode 字符串占用太多内存——特别是与 Python 2.x 相比,在 Python 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 中的表示(以 null 结尾)。如果 wchar_t 是 16 位,则此形式可以使用代理对(在这种情况下,wstr_length 与 length 不同)。只有当表示中存在代理对时,wstr_length 才会与 length 不同。
- utf8_length, utf8:UTF-8 表示(以 null 结尾)。
- data:Unicode 字符串的最短形式表示。字符串以 null 结尾(在其各自的表示中)。
所有三种表示都是可选的,尽管 data 形式被认为是规范表示,只有在创建字符串时才会不存在。如果表示不存在,则指针为 NULL,并且相应的长度字段可能包含任意数据。
Py_UNICODE 类型仍然受支持,但已弃用。它始终定义为 wchar_t 的 typedef,因此 wstr 表示可以兼作 Py_UNICODE 表示。
如果字符串仅使用 ASCII 字符,则 data 和 utf8 指针指向同一内存(仅使用 Latin-1 不够)。如果字符串恰好完全适合平台的 wchar_t 类型(即,如果 sizeof(wchar_t) 为 2,则使用一些 BMP 非 Latin-1 字符,如果 sizeof(wchar_t) 为 4,则使用一些非 BMP 字符),则 data 和 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 相同,后者已被移除。该函数将在第一次调用时计算 utf8 表示。由于此表示将消耗内存,直到字符串对象被释放,因此应用程序应尽可能使用现有的 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 实例。它已更新以跟踪更改。
弃用、移除和不兼容性
虽然此 PEP 已弃用 Py_UNICODE 表示和 API,但未计划移除相应的 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 的扩展模块可能会意外地调用 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字节。只有在与宽Unicode构建进行比较时,3.x使用PEP与2.x相比才会出现这种减少。
移植指南
只有少部分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分配目标字符串,并第二次遍历输入以生成最终输出。虽然这听起来可能很昂贵,但实际上它可能比在以下方法中再次复制结果更便宜。
如果您采用启发式方法,请避免分配旨在调整大小的字符串,因为调整字符串的大小对于其规范表示形式不起作用。相反,分配一个单独的缓冲区来收集字符,然后使用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
上次修改时间: 2023-09-09 17:39:29 GMT