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):是否数据已准备完成- 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算
read
y
- 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算
- 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
字符串,循环处理剩余的字符,写入到buffer
PyUnicodeWriter
实例默认的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
字符串实例就生成了。