python的字符串实质到底是什么类型的数据,这个可是困扰着很多编程者的话题。在python2我们已经被中文编码相关的问题折磨的不轻,那到了python3之后为什么又解决了这个问题呢?今天这篇文章就带大家详细剖析python3的字符串实现。
我们首先看一段代码:
1 | def test_str_basic(): |
这段代码打印了一个字符串对象的类型,其结果为<class 'str'>。str类型从哪里来?从C源码中我们可以搜索到,其来源于unicodeobject.c的PyUnicode_Type
1 | // unicodeobject.c |
对应地,其大小为PyUnicodeObject所占的大小。PyUnicodeObject可表示的数据结构如下:
1 | // unicodeobject.h |
其中,PyUnicodeObject、PyCompactUnicodeObject、PyASCIIObject分别对应三种不同的字符串类型,它们有以下区别:
PyASCIIObject:通过PyUnicode_New生成的只包含ASCII字符的字符串,字符数据会紧随结构体排列PyCompactUnicodeObject:通过PyUnicode_New生成的包含非ASCII字符的字符串,字符数据会紧随结构体排列PyUnicodeObject:通过PyUnicode_FromUnicode创建
其中PyUnicode_FromUnicode是已经过时的方法,因此我们常见的unicode对象是以PyASCIIObject、PyCompactUnicodeObject的形式存在的,也就是说现在我们日常创建的字符串对象,其数据会紧随结构体排列。
python的unicode实现是对标标准unicode规范的,因此要深入了解其中原理,我们需要预备一些关于unicode的知识,比如:
- unicode tutorial
- unicode wiki
- 彻底弄懂unicode编码
unicode本身存在的意义是将世界上任意一个字符(包括emoji)映射到一个特定的数字,这个数字被称为code point。unicode的code point是分组的,每组65536个,称作为一个个plane。每个unicode字符用4个字节表示,但如果需要进行二进制编码的话,比如存储文件或是网络传输,每个字符都使用4个字节往往会有冗余,因此需要一个比较效率的二进制编码方式,比如:utf8、utf16。
utf8编码是最常见的编码之一,其长度可变,但针对不同unicode的code point值,会编码成不同长度的形式,比如ASCII支持的英文在utf8编码里只占1个字节,但汉字可能就是3个字节。在python中,当我们需要把字符串编码成utf8的二进制形式时,就需要string.encode('utf-8')的调用,反之假设我们从一个socket接收到utf8编码的数据,需要解码成字符串时,就需要bytestring.decode('utf-8')对字符串进行解码。
言归正传,现在我们回到对PyASCIIObject、PyCompactUnicodeObject数据结构的研究当中。首先我们看PyASCIIObject,它有以下几个部分:
length:字符串的长度,按code point个数计算hash:字符串的hash编码state:字符串状态,用一个完整4字节存储interned(2):是否被短字符串缓存,以及是否永久缓存kind(3):字符串的类型- 0:
wide-char宽字符类型 - 1:
1byte,全部字符都可用8bits无符号表示 - 2:
2byte,全部字符都可用16bits无符号表示 - 4:
4byte,全部字符都可用32bits无符号表示
- 0:
compact(1):字符串数据是否紧凑于结构体排列- 先前提到
PyASCIIObject、PyCompactUnicodeObject都是紧凑排列
- 先前提到
ascii(1):是否只有ascii字符- 先前
kind=1的时候由于是unsigned,因此可以表示除ascii外到200多的字符
- 先前
ready(1):是否数据已准备完成- 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算
ready
- 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算
- 24位
padding
wstr:wide-char的字符串表示
PyASCIIObject用来表示ASCII字符,而含有非ASCII字符的字符串则用PyCompactUnicodeObject表示,其包含以下内容:
_base:PyASCIIObject的实例utf8_length:除了\0之外,utf8的字符串表示的比特数utf8:utf8的字符串表示wstr_length:wide-char字符串表示里code point的个数
而字符串的真实数据则放到了PyUnicodeObject的data当中,以一个union的形式表示不同长度表示的字符串
接下来我们通过一个例子来展示unicode字符串是如何被创建的。我们的代码是字母+汉字+数字:
1 | s = "abc哈咯123" |
当这段代码被打入到解释器中,被词法分析器分析时,就会调用字符串创建的逻辑unicode_decode_utf8,将const char类型的原生字符转化为PyUnicodeObject
1 | // unicodeobject.c |
unicode_decode_utf8做了以下几件事情:
- 当
size为1并且第一个字符值小于128时,通过get_latin1_char方法获取unicode实例 - 采用
PyUnicode_New初始化unicode实例并预设最大字符值为127,然后先尝试用ascii_decode将原生字符串转换为一个ascii的unicode实例- 如果成功就
return了 - 如果没成功,
s会停在第一个非ascii字符的前面- 在上面的例子里,
s也就表示"哈咯123"
- 在上面的例子里,
- 如果成功就
- 以先前的
unicode实例为buffer,初始化PyUnicodeWriter实例处理非ASCII字符串,循环处理剩余的字符,写入到bufferPyUnicodeWriter实例默认的kind为PyUnicode_1BYTE_KIND。一般第一个字符会走到asciilib_utf8_decode逻辑,这个逻辑如果发现字符越界,会返回字符实际的code point值
- 发现第一个字符越界不能用
ASCII表示,调用_PyUnicodeWriter_WriteCharInline逻辑写入字符- 调用
_PyUnicodeWriter_Prepare逻辑,其中会根据第一个字符的值大小决定writer写入的字符类型kind- 汉字
"哈"对应code point值为27014,即\u54c8。因此writer将kind调整到PyUnicode_2BYTE_KIND,以适配汉字"哈"的写入
- 汉字
- 调用
PyUnicode_WRITE,写入字符"哈"到buffer
- 调用
writer.kind为PyUnicode_2BYTE_KIND,下一个循环之后,调用ucs2lib_utf8_decode方法- 由于汉字基本是
PyUnicode_2BYTE_KIND,通过ucs2lib_utf8_decode方法,就能把后面所有的字符都进行处理
- 由于汉字基本是
- 调用
_PyUnicodeWriter_Finish,生成最终的unicode实例- 调用
resize_compact,重新调整unicode实例数据
- 调用
通过以上的操作,一个unicode字符串实例就生成了。