# str
# python中的unicode
Python 在 3 之后,str 对象内部改用 Unicode 表示,因而被源码称为 Unicode 对象。这么做的好处是程序核心逻辑统一用 Unicode ,只需在输入、输入层进行编码、解码,可最大程度避免各种编码问题。
由于Unicode 收录字符已经超过 13 万个,每个字符至少需要 4 个字节来保存。cpython源码中对此做了一定优化-根据文本内容选择底层存储单元。
- Go对此类似的全部采用了UTF-8编码来解决这个问题字符串
- 可考虑变长存储-英文1字节,中文2字节,但是采用变长存储单元后,就无法在 O(1) 时间内取出文本第 n 个字符—只能从头遍历直到第 n 个字符。
>>> import sys # 英文字符需要1字节 >>> sys.getsizeof('ab') - sys.getsizeof('a') 1 # 中文字符需要2字节 >>> sys.getsizeof('中国') - sys.getsizeof('中') 2 # Emoji表情需要4字节 # 注意这里是显示问题所以实际测试时直接复制代码的话结果为1 >>> sys.getsizeof('??') - sys.getsizeof('?') 4
# 内部实现
# 文本类型与存储结果
在 Include/unicodeobject.h 头文件中,可以发现 str 对象底层存储根据文本字符 Unicode 码位范围分成几类:
-
PyUnicode_1BYTE_KIND ,所有字符码位均在 U+0000 到 U+00FF 之间,对此使用uint_8来进行存储。 -
PyUnicode_2BYTE_KIND ,所有字符码位均在 U+0000 到 U+FFFF 之间,且至少一个大于 U+00FF,对此使用uint_16来进行存储。 -
PyUnicode_4BYTE_KIND ,所有字符码位均在 U+0000 到 U+10FFFF 之间,且至少一个大于 U+FFFF,对此使用uint_32来进行存储。
| 文本类型 | 字符存储单元 | 字符存储单元大小(字节) |
|---|---|---|
| PyUnicode_1BYTE_KIND | Py_UCS1 | 1 |
| PyUnicode_2BYTE_KIND | Py_UCS2 | 2 |
| PyUnicode_4BYTE_KIND | Py_UCS4 | 4 |
enum PyUnicode_Kind {
/* String contains only wstr byte characters. This is only possible
when the string was created with a legacy API and _PyUnicode_Ready()
has not been called yet. */
PyUnicode_WCHAR_KIND = 0,
/* Return values of the PyUnicode_KIND() macro: */
PyUnicode_1BYTE_KIND = 1,
PyUnicode_2BYTE_KIND = 2,
PyUnicode_4BYTE_KIND = 4
};
typedef uint32_t Py_UCS4;
typedef uint16_t Py_UCS2;
typedef uint8_t Py_UCS1;
Python Unicode内部存储结果因文本类型而不同,因此采用了哪种字符存储单元必须作为Unicode公共字段进行保存,类似的还有:
- interned ,是否为 interned 机制维护
- kind ,类型,用于区分字符底层存储单元大小
- compact ,内存分配方式,对象与文本缓冲区是否分离
- ascii ,文本是否均为纯 ASCII
Objects/unicodectype.c 源文件中的 PyUnicode_New 函数,根据文本字符数 size以及最大字符 maxchar初始化 Unicode对象。该函数根据 maxchar为 Unicode对象选择最紧凑的字符存储单元以及底层结构体:
maxchar < 128 | maxchar < 256 | maxchar < 65536 | maxchar < MAX_UNICODE | |
|---|---|---|---|---|
| kind | PyUnicode_1BYTE_KIND | PyUnicode_1BYTE_KIND | PyUnicode_2BYTE_KIND | PyUnicode_4BYTE_KIND |
| ascii | 1 | 0 | 0 | 0 |
| 字符存储单元大小 | 1 | 1 | 2 | 4 |
| 底层结构体 | PyASCIIObject | PyCompactUnicodeObject | PyCompactUnicodeObject | PyCompactUnicodeObject |
根据试验可以看出,在指定了kind类型后,其内部所有字符都以这种方式进行保存。
>>> sys.getsizeof('')
49
>>> sys.getsizeof('中')
76
>>> sys.getsizeof('中国')
78
>>> sys.getsizeof('中国a')
80
>>> sys.getsizeof('a')
50
>>> sys.getsizeof('ab')
51
>>> sys.getsizeof('a中')
78
>>> sys.getsizeof('ab中')
80
# PyASCIIObject
- 如果 str 对象保存的文本均为
ASCII,即maxchar<128,则底层由PyASCIIObject 结构存储 - 同时
PyASCIIObject也似乎其他Unicode底层存储结构体的基础,所有字段均为Unicode公共字段。 - 注意,与
Unicode一致,PyASCIIObject也会在结构体末尾加上\0字节以此兼容C字符串。 - 因此对于长度为n的纯ANCII字符串对象,需要消耗
n+48+1字节的内存空间。 - 注意,字符串虽然是变长对象,但是使用的是
PyObject_HEAD,因为PyObject_VAR_HEAD用于描述每个元素大小都一样的变长对象,元素大小由类型对象tp_itemsize字段描述。而str对象,每个元素(字符)到底用多大的存储单元,与字符范围有关,因此底层作了特殊处理。
/* ASCII-only strings created through PyUnicode_New use the PyASCIIObject
structure. state.ascii and state.compact are set, and the data
immediately follow the structure. utf8_length and wstr_length can be found
in the length field; the utf8 pointer is equal to the data pointer. */
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* Number of code points in the string */
Py_hash_t hash; /* Hash value; -1 if not set */
struct {
unsigned int interned:2;
unsigned int kind:3;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
unsigned int :24;
} state;
wchar_t *wstr; /* wchar_t representation (null-terminated) */
} PyASCIIObject;
注意会出现一个4字节的内存空洞做内存对齐用。
# PyCompactUnicodeObject
如果文本不全是ANCII,
Unicode对象底层便由PyCompactUnicodeObject结构体来保存。
PyCompactUnicodeObject结构体在PyASCIIObject结构体的基础上增加了utf8_length、utf_7、wstr_length三个字段分别保存文本 UTF8 编码长度以及文本 UTF8 编码形式(wstr_length暂不涉及)。- ASCII本身即是合法的UTF8,无需保存UTF8编码形式,这也是ASCII文本底层由PyASCIIObject保存的原因。
/* Non-ASCII strings allocated through PyUnicode_New use the
PyCompactUnicodeObject structure. state.compact is set, and the data
immediately follow the structure. */
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
* terminating \0. */
char *utf8; /* UTF-8 representation (null-terminated) */
Py_ssize_t wstr_length; /* Number of code points in wstr, possible
* surrogates count as two code points. */
} PyCompactUnicodeObject;
# 内存优化
# interned机制 - 编译时优化
注意,这种优化可能随着版本变更而变化,不可以将程序逻辑建立在这些优化之上。
考虑如下场景,如果程序中存在大量User对象,有什么可优化的地方:
>>> class User:
...
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
>>>
>>> user = User(name='tom', age=20)
>>> user.__dict__
{'name': 'tom', 'age': 20}
由于对象的属性由dict保存,这意味着每个User对象都将保存str对象name,即使是一样的name也会创建很多份,将会浪费不少内存空间。
因为str是不可变对象,因此Python有潜在可能将重复的字符串做成单例模式,即interned机制。
具体做法是在内部为一个全局dict对象,所有开启interned机制的str对象均保存在这里,后续需要用到相关对象的地方则优先到全局dict中取,避免重复创建。
这个dict对象key、value都是被缓存的str对象,大致思路:
Python先创建一个字符串对象s;
dict里面可能已经缓存了相同的对象ss;
Python执行dict.setdefault(s, s),这一步分为两种情况:
- dict缓存了相同对象ss,setdefault返回ss,Python那ss替换s,这样就保证相同str对象只有一个实例
- 如果dict未缓存s,就会缓存s,key和value都是s,这样一来后续创建跟s相同的新对象时,都会被s替换。
例如:
s1 = 'a' + 'b' + 'c'
s2 = 'ab' + 'c'
s3 = 'abc'
s4 = 'a' + 'bc'
s5 = 'abc' + ''
p = ''
s6 = 'abc' + p
for item in [s1, s2, s3, s4, s5, s6]:
print(id(item))
"""
140699274168160
140699274168160
140699274168160
140699274168160
140699274168160
140699274168160
"""
# QA
# 为什么使用PyObject_HEAD而不是PyObject_VAR_HEAD
PyObject_VAR_HEAD用于描述每个元素大小都一样的变长对象,元素大小由类型对象tp_itemsize字段描述。而str对象,每个元素(字符)到底用多大的存储单元,与字符范围有关,因此底层作了特殊处理。
#TODO#