【Hard Python】【第五章-字符串】1、unicode,py3的字符串实现

python的字符串实质到底是什么类型的数据,这个可是困扰着很多编程者的话题。在python2我们已经被中文编码相关的问题折磨的不轻,那到了python3之后为什么又解决了这个问题呢?今天这篇文章就带大家详细剖析python3的字符串实现。

我们首先看一段代码:

1
2
3
def test_str_basic():
s = '123456789'
print(type(s))

这段代码打印了一个字符串对象的类型,其结果为<class 'str'>str类型从哪里来?从C源码中我们可以搜索到,其来源于unicodeobject.cPyUnicode_Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// unicodeobject.c
PyTypeObject PyUnicode_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"str", /* tp_name */
sizeof(PyUnicodeObject), /* tp_basicsize */
0, /* tp_itemsize */
/* Slots */
(destructor)unicode_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
unicode_repr, /* tp_repr */
&unicode_as_number, /* tp_as_number */
&unicode_as_sequence, /* tp_as_sequence */
&unicode_as_mapping, /* tp_as_mapping */
(hashfunc) unicode_hash, /* tp_hash*/
0, /* tp_call*/
(reprfunc) unicode_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_UNICODE_SUBCLASS |
_Py_TPFLAGS_MATCH_SELF, /* tp_flags */
unicode_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
PyUnicode_RichCompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
unicode_iter, /* tp_iter */
0, /* tp_iternext */
unicode_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
&PyBaseObject_Type, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
unicode_new, /* tp_new */
PyObject_Del, /* tp_free */
};

对应地,其大小为PyUnicodeObject所占的大小。PyUnicodeObject可表示的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// unicodeobject.h
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;


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;


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;

其中,PyUnicodeObjectPyCompactUnicodeObjectPyASCIIObject分别对应三种不同的字符串类型,它们有以下区别:

  • PyASCIIObject:通过PyUnicode_New生成的只包含ASCII字符的字符串,字符数据会紧随结构体排列
  • PyCompactUnicodeObject:通过PyUnicode_New生成的包含非ASCII字符的字符串,字符数据会紧随结构体排列
  • PyUnicodeObject:通过PyUnicode_FromUnicode创建

其中PyUnicode_FromUnicode是已经过时的方法,因此我们常见的unicode对象是以PyASCIIObjectPyCompactUnicodeObject的形式存在的,也就是说现在我们日常创建的字符串对象,其数据会紧随结构体排列。

pythonunicode实现是对标标准unicode规范的,因此要深入了解其中原理,我们需要预备一些关于unicode的知识,比如:

  • unicode tutorial
  • unicode wiki
  • 彻底弄懂unicode编码

unicode本身存在的意义是将世界上任意一个字符(包括emoji)映射到一个特定的数字,这个数字被称为code pointunicodecode point是分组的,每组65536个,称作为一个个plane。每个unicode字符用4个字节表示,但如果需要进行二进制编码的话,比如存储文件或是网络传输,每个字符都使用4个字节往往会有冗余,因此需要一个比较效率的二进制编码方式,比如:utf8utf16

utf8编码是最常见的编码之一,其长度可变,但针对不同unicodecode point值,会编码成不同长度的形式,比如ASCII支持的英文在utf8编码里只占1个字节,但汉字可能就是3个字节。在python中,当我们需要把字符串编码成utf8的二进制形式时,就需要string.encode('utf-8')的调用,反之假设我们从一个socket接收到utf8编码的数据,需要解码成字符串时,就需要bytestring.decode('utf-8')对字符串进行解码。

言归正传,现在我们回到对PyASCIIObjectPyCompactUnicodeObject数据结构的研究当中。首先我们看PyASCIIObject,它有以下几个部分:

  • length:字符串的长度,按code point个数计算
  • hash:字符串的hash编码
  • state:字符串状态,用一个完整4字节存储
    • interned(2):是否被短字符串缓存,以及是否永久缓存
    • kind(3):字符串的类型
      • 0:wide-char宽字符类型
      • 1:1byte,全部字符都可用8bits无符号表示
      • 2:2byte,全部字符都可用16bits无符号表示
      • 4:4byte,全部字符都可用32bits无符号表示
    • compact(1):字符串数据是否紧凑于结构体排列
      • 先前提到PyASCIIObjectPyCompactUnicodeObject都是紧凑排列
    • ascii(1):是否只有ascii字符
      • 先前kind=1的时候由于是unsigned,因此可以表示除ascii外到200多的字符
    • ready(1):是否数据已准备完成
      • 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算ready
    • 24位padding
  • wstrwide-char的字符串表示

PyASCIIObject用来表示ASCII字符,而含有非ASCII字符的字符串则用PyCompactUnicodeObject表示,其包含以下内容:

  • _basePyASCIIObject的实例
  • utf8_length:除了\0之外,utf8的字符串表示的比特数
  • utf8utf8的字符串表示
  • wstr_length:wide-char字符串表示里code point的个数

而字符串的真实数据则放到了PyUnicodeObjectdata当中,以一个union的形式表示不同长度表示的字符串

接下来我们通过一个例子来展示unicode字符串是如何被创建的。我们的代码是字母+汉字+数字:

1
s = "abc哈咯123"

当这段代码被打入到解释器中,被词法分析器分析时,就会调用字符串创建的逻辑unicode_decode_utf8,将const char类型的原生字符转化为PyUnicodeObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// unicodeobject.c
static PyObject *
unicode_decode_utf8(const char *s, Py_ssize_t size,
_Py_error_handler error_handler, const char *errors,
Py_ssize_t *consumed)
{
if (size == 0) {
if (consumed)
*consumed = 0;
_Py_RETURN_UNICODE_EMPTY();
}

/* ASCII is equivalent to the first 128 ordinals in Unicode. */
if (size == 1 && (unsigned char)s[0] < 128) {
if (consumed) {
*consumed = 1;
}
return get_latin1_char((unsigned char)s[0]);
}

const char *starts = s;
const char *end = s + size;

// fast path: try ASCII string.
PyObject *u = PyUnicode_New(size, 127);
if (u == NULL) {
return NULL;
}
s += ascii_decode(s, end, PyUnicode_1BYTE_DATA(u));
if (s == end) {
return u;
}

// Use _PyUnicodeWriter after fast path is failed.
_PyUnicodeWriter writer;
_PyUnicodeWriter_InitWithBuffer(&writer, u);
writer.pos = s - starts;

Py_ssize_t startinpos, endinpos;
const char *errmsg = "";
PyObject *error_handler_obj = NULL;
PyObject *exc = NULL;

while (s < end) {
Py_UCS4 ch;
int kind = writer.kind;

if (kind == PyUnicode_1BYTE_KIND) {
if (PyUnicode_IS_ASCII(writer.buffer))
ch = asciilib_utf8_decode(&s, end, writer.data, &writer.pos);
else
ch = ucs1lib_utf8_decode(&s, end, writer.data, &writer.pos);
} else if (kind == PyUnicode_2BYTE_KIND) {
ch = ucs2lib_utf8_decode(&s, end, writer.data, &writer.pos);
} else {
assert(kind == PyUnicode_4BYTE_KIND);
ch = ucs4lib_utf8_decode(&s, end, writer.data, &writer.pos);
}

switch (ch) {
case 0:
if (s == end || consumed)
goto End;
errmsg = "unexpected end of data";
startinpos = s - starts;
endinpos = end - starts;
break;
case 1:
errmsg = "invalid start byte";
startinpos = s - starts;
endinpos = startinpos + 1;
break;
case 2:
if (consumed && (unsigned char)s[0] == 0xED && end - s == 2
&& (unsigned char)s[1] >= 0xA0 && (unsigned char)s[1] <= 0xBF)
{
/* Truncated surrogate code in range D800-DFFF */
goto End;
}
/* fall through */
case 3:
case 4:
errmsg = "invalid continuation byte";
startinpos = s - starts;
endinpos = startinpos + ch - 1;
break;
default:
if (_PyUnicodeWriter_WriteCharInline(&writer, ch) < 0)
goto onError;
continue;
}

// case 1、3、4的逻辑,会获取不同类型的error_handler,这里先忽略
}

End:
if (consumed)
*consumed = s - starts;

Py_XDECREF(error_handler_obj);
Py_XDECREF(exc);
return _PyUnicodeWriter_Finish(&writer);

onError:
Py_XDECREF(error_handler_obj);
Py_XDECREF(exc);
_PyUnicodeWriter_Dealloc(&writer);
return NULL;
}

unicode_decode_utf8做了以下几件事情:

  • size为1并且第一个字符值小于128时,通过get_latin1_char方法获取unicode实例
  • 采用PyUnicode_New初始化unicode实例并预设最大字符值为127,然后先尝试用ascii_decode将原生字符串转换为一个asciiunicode实例
    • 如果成功就return
    • 如果没成功,s会停在第一个非ascii字符的前面
      • 在上面的例子里,s也就表示"哈咯123"
  • 以先前的unicode实例为buffer,初始化PyUnicodeWriter实例处理非ASCII字符串,循环处理剩余的字符,写入到buffer
    • PyUnicodeWriter实例默认的kindPyUnicode_1BYTE_KIND。一般第一个字符会走到asciilib_utf8_decode逻辑,这个逻辑如果发现字符越界,会返回字符实际的code point
  • 发现第一个字符越界不能用ASCII表示,调用_PyUnicodeWriter_WriteCharInline逻辑写入字符
    • 调用_PyUnicodeWriter_Prepare逻辑,其中会根据第一个字符的值大小决定writer写入的字符类型kind
      • 汉字"哈"对应code point值为27014,即\u54c8。因此writerkind调整到PyUnicode_2BYTE_KIND,以适配汉字"哈"的写入
    • 调用PyUnicode_WRITE,写入字符"哈"buffer
  • writer.kindPyUnicode_2BYTE_KIND,下一个循环之后,调用ucs2lib_utf8_decode方法
    • 由于汉字基本是PyUnicode_2BYTE_KIND,通过ucs2lib_utf8_decode方法,就能把后面所有的字符都进行处理
  • 调用_PyUnicodeWriter_Finish,生成最终的unicode实例
    • 调用resize_compact,重新调整unicode实例数据

通过以上的操作,一个unicode字符串实例就生成了。

版权声明
本文为博客HiKariのTechLab原创文章,转载请标明出处,谢谢~~~