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

Python 增强提案

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