前言
本文同时发布于笔者的《对Python内核的理解 - 连载 - 简书》文集一同发布
在CPython3.3之后,字符串对象发生了根本性的变法,本篇我们来讨论一下字符串对象,在Include/unicodeobject.h,在整个源代码的官方文档可以归纳出几点。在CPython3.3+之后,Unicode字符串分为有4种
紧凑型ASCII(Compact ASCII)
紧凑型ASCII也称为ASCII限定字符串(ASCII only String).其对应PyASCIIObject结构体,该对象使用一个空间连续的内存块(一个内部的state结构体和一个wchar_t类型的指针),紧凑型ASCII只能涵盖拉丁编码以内的字符。ASCII字符限定意味着PyASCIIObject只能U+0000 ~ U+007F这段区间的字符码。
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* 字符串中的码位个数 */
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; /*C底层的宽字符序列以NUL结束*/
} PyASCIIObject;
ASCII限定字符串可以由PyUnicode_New函数使用其结构体创建并设定state.ascii为1,state.compact为1。
从上面的类定义可知
- length用于保存字符串中字符编码的数量
- hash用于缓存C级别字符串的哈系值。由于字符串对象是不可变对象,这样避免每次重新计算该字符串的hash字段的值
- state保存了保存了其子类实例的状态信息,
- wstr是缓存C字符串的一个wchar指针,当然它是以“0”结束
紧凑型Unicode(Compact Unicode)
其对应PyCompactUnicodeObject结构体,紧凑型Unicode以PyASCIIObject为基类,非ASCII字符串可以通过PyUnicode_New函数为PyCompactUnicodeObject分配内存并设置state.compact=1
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* utf8中的字节数,不包括结尾的0. */
char *utf8; /* UTF-8表示形式(0终止) */
Py_ssize_t wstr_length; /* wstr中的码位个数 */
} PyCompactUnicodeObject;
传统的字符串(Legacy String)
其对应PyUnicodeObject结构体,传统的字符串对象会其中会包含两种特殊状态not ready和ready。
传统的字符串可以通过PyUnicode_FromUnicode为分配PyUnicodeObject结构体分配内存并封装C级别的unicode字符串。 实际的字符串数据最初位于wstr块中,并使用_PyUnicode_Ready函数复制到data的块中。
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* 最小形式的Unicode缓冲区 */
} PyUnicodeObject;
Unicode对象的原始基类除了PyObject外,是以PyASCIIObject继承而来的,PyCompactUnicodeObject类继承PyASCIIObject,PyUnicodeObject继承自PyCompactUnicodeObject,那么整个CPython3.3+的字符串体系可以用如下图表示
![0ac7a9a2f3d35ce04c4a712a079c52f3.png](https://i-blog.csdnimg.cn/blog_migrate/7f2ca77331913d2cc41aa92e2c0a5d58.jpeg)
Unicode字符串的字节宽度
在了解字符串如何创建有一个非常关键概念,我们查看Include/cpython/unicodeobject.h源文件时,CPython内部定义了一个叫PyUnicode_Kind的枚举类型,PyUnicode_New函数在实例化一个字符串对象时,会使用PyUnicode_Kind的枚举值设定字符串对象内部类state.kind的值,该字段将告知CPython的其他内部代码如何解读C底层的char指针指向的字符串数据。
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
};
字符串对象的内存分配
前文说到PyASCIIObject对象和PyCompactUnicodeObject对象都可以通过PyUnicode_New函数来创建,那么该函数如何区分它创建的目标是PyASCIIObject,还是PyCompactUnicodeObject呢?尽管两者是"父子"的继承关系,毕竟它们是不同的数据类型,仔细看一下实现代码,大体上PyUnicode_New函数是根据maxchar来区分创建什么字符串对象的。
- maxchar小于128,并且字符位宽为1个字节,即标准的ASCII可识别的有效字符仅有128个,于是创建PyASCIIObject对象
- maxchar小于256,并且字符位宽为1个字节,PyUnicode_New就创建PyCompactUnicodeObject对象。对于256个字符码位组成的字符集,称为扩展的ASCII字符集(Extended ASCII Charset)
字节通常用于保存文本文档中的各个字符。 在ASCII字符集中,每个0到127之间的二进制值都被赋予一个特定字符。 大多数计算机扩展了ASCII字符集,以使用一个字节中可用的256个字符的整个范围。 前128个字符处理特殊内容,例如常见外语中的重音字符。
- maxchar小于65536,并且字符位宽为2个字节,PyUnicode_New就创建PyCompactUnicodeObject对象,这种情况PyCompactUnicodeObject对象实际保存的是utf-16编码的字符串。
- 最后一种情况就是处理码位个数大于65536且小于MAX_UNICODE,通常此类的字符串的编码是utf-32
PyObject *
PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar)
{
PyObject *obj;
PyCompactUnicodeObject *unicode;
void *data;
enum PyUnicode_Kind kind;
int is_sharing, is_ascii;
Py_ssize_t char_size;
Py_ssize_t struct_size;
/*返回空字符串的PyObject包装类 */
if (size == 0 && unicode_empty != NULL) {
Py_INCREF(unicode_empty);
return unicode_empty;
}
//处理ASCII字符集
is_ascii = 0;
is_sharing = 0;
struct_size = sizeof(PyCompactUnicodeObject);
if (maxchar < 128) {
kind = PyUnicode_1BYTE_KIND;
char_size = 1;
is_ascii = 1;
struct_size = sizeof(PyASCIIObject);
}
//处理ASCII扩展的字符集
else if (maxchar < 256) {
kind = PyUnicode_1BYTE_KIND;
char_size = 1;
}
//处理utf-16编码的字符集
else if (maxchar < 65536) {
kind = PyUnicode_2BYTE_KIND;
char_size = 2;
if (sizeof(wchar_t) == 2)
is_sharing = 1;
}
//处理utf-32编码的字符串
else {
if (maxchar > MAX_UNICODE) {
PyErr_SetString(PyExc_SystemError,
"invalid maximum character passed to PyUnicode_New");
return NULL;
}
kind = PyUnicode_4BYTE_KIND;
char_size = 4;
if (sizeof(wchar_t) == 4)
is_sharing = 1;
}
/* Ensure we won't overflow the size. */
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyUnicode_New");
return NULL;
}
if (size > ((PY_SSIZE_T_MAX - struct_size) / char_size - 1))
return PyErr_NoMemory();
/*
来自_PyObject_New()的重复分配代码,而不是对PyObject_New()的调用,
因此我们能够为对象及其数据缓冲区分配空间。
*/
obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);
if (obj == NULL)
return PyErr_NoMemory();
//绑定PyUnicode_Type的类型信息
obj = PyObject_INIT(obj, &PyUnicode_Type);
if (obj == NULL)
return NULL;
unicode = (PyCompactUnicodeObject *)obj;
if (is_ascii)
//obj指针移动
data = ((PyASCIIObject*)obj) + 1;
else
data = unicode + 1;
//设定state内部类的状态信息
_PyUnicode_LENGTH(unicode) = size;
_PyUnicode_HASH(unicode) = -1;
_PyUnicode_STATE(unicode).interned = 0;
_PyUnicode_STATE(unicode).kind = kind;
_PyUnicode_STATE(unicode).compact = 1;
_PyUnicode_STATE(unicode).ready = 1;
_PyUnicode_STATE(unicode).ascii = is_ascii;
if (is_ascii) {
//NULL结束符
((char*)data)[size] = 0;
_PyUnicode_WSTR(unicode) = NULL;
}
else if (kind == PyUnicode_1BYTE_KIND) {
((char*)data)[size] = 0;
_PyUnicode_WSTR(unicode) = NULL;
_PyUnicode_WSTR_LENGTH(unicode) = 0;
unicode->utf8 = NULL;
unicode->utf8_length = 0;
}
else {
unicode->utf8 = NULL;
unicode->utf8_length = 0;
if (kind == PyUnicode_2BYTE_KIND)
((Py_UCS2*)data)[size] = 0;
else /* kind == PyUnicode_4BYTE_KIND */
((Py_UCS4*)data)[size] = 0;
if (is_sharing) {
_PyUnicode_WSTR_LENGTH(unicode) = size;
_PyUnicode_WSTR(unicode) = (wchar_t *)data;
}
else {
_PyUnicode_WSTR_LENGTH(unicode) = 0;
_PyUnicode_WSTR(unicode) = NULL;
}
}
#ifdef Py_DEBUG
unicode_fill_invalid((PyObject*)unicode, 0);
#endif
assert(_PyUnicode_CheckConsistency((PyObject*)unicode, 0));
return obj;
}
PyUnicode_New函数在计算要为字符串对象分配的内存后,即执行下面这条语句后
obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);
那么PyASCIIObject的内存分配如下图
![ec095ca07f35400137eab56f9170ecbc.png](https://i-blog.csdnimg.cn/blog_migrate/61029bc01a1bbbf14342c7e0bfa2f894.png)
跟着会调用PyObject_INIT(obj, &PyUnicode_Type)函数来将PyUnicode_Type实例绑定到字符串对象的头部。
OK!我们之前谈论PyType_Type实例和各内置数据类型的关系后,你应该清楚字符串对象的初始化匹配对应的PyUnicode_Type实例,我们关注的是tp_new字段的函数指针unicode_new
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 */
.....
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*/
....
(reprfunc) unicode_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
....
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_UNICODE_SUBCLASS, /* tp_flags */
unicode_doc, /* tp_doc */
.....
PyUnicode_RichCompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
unicode_iter, /* tp_iter */
0, /* tp_iternext */
unicode_methods, /* tp_methods */
....
&PyBaseObject_Type, /* tp_base */
....
unicode_new, /* tp_new */
PyObject_Del, /* tp_free */
};
若我们为以下字符串,分配内存,对于CPython来说,它们默认执行utf-8执行解码也即29个字节
"我是一个自由开发者!!"
当整个PyUnicode_New函数返回时,它构建的PyASCIIObject如下内存图所示
![692bfd93a99945d1a1716eeefd93f86a.png](https://i-blog.csdnimg.cn/blog_migrate/71cc13e791e93d8dfc914bfc99d7d785.jpeg)
字符串对象的初始化
一个简单的例子,有想过在一个Python脚本中,一个字符串字面量如何在CPython内部完成字符串对象的实例化吗?对于CPython3.9来说,在实例化一个脚本内固有的字符串(即单引号或双引号内),其实质上从C级别的字符指针(const char*)指向的字符串字面量拷贝到PyUnicode_New函数分配的堆内存的过程。而字符串初始化的函数调用起点为PyUnicode_DecodeUTF8Stateful函数。
该流程省略了很多unicode字节码解码等特殊情况而得到一个简化的流程图。经过测试,几乎所有Python脚本内部所有字符串初始化的常规函数调用流程。
![dfccc984634100438bbbd8b3e1dde186.png](https://i-blog.csdnimg.cn/blog_migrate/610e8b16ae854281a8317c4c7d919aee.jpeg)
有人可能会问,你这个图依据是怎么来的?我们已经知道PyUnicode_New函数是一个为字符串对象间接分配内存的函数接口,我们只要通过IDE工具查找并筛选引用该函数的上一个函数的结果,从中找到可能的函数调用路径,并在各个可能的函数中插入一些printf函数,打印函数名称和相关传入的关键参数,就能推断出该字符串对象初始化的轨迹了。还有慎用Python的Debug模型,因为你从IDE工具看到内存状态可能和运行时有所差异的。这个我在其他篇章也提到过。例如,我们在一个测试的test.py文件中,测试下面的Python字符串的实例化过程
"我是一个自由开发者!!"
那么执行python脚本将所有打印的运行时信息重定向到一个文本中
./python test.py >debug.txt
如下图所示,我们发现只要python的运行时系统不论调用模块间的内置函数,还是用户的自定义函数,只要涉及Python字符串对象都依次遵循上面PyASCIIObject/PyUnicodeObject初始化的函数调用过程
unicode_decode_utf8函数
回归正题,我们先看一下一个关键的函数unicode_decode_utf8,该函数的完整代码见Objects/unicodeobject.c的第4979行-5122行,由于篇幅所限我这里将该函数拆解三个部分来讨论,先查看第4979行第5088行.该函数第一个参数是const char*类型字符指针s,这里重点讨论该函数和它调用的ascii_decode函数的一些细节问题。
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字符 */
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;
//假定参数s是一堆由ASCII码位组成的字符串
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;
}
....
}
unicode_decode_utf8函数假定传入的C级别的字符串分三种情况实例化字符串对象
第1种情况:仅包含一个字符且位于标准的ASCII字符集区间内
此时调用get_latin1_char函数并返回,那么get_latin1_char函数主要做的事情就是在整个Python解释器运行期间的缓存所有使用过的单个ASCII字符对象到一个长度为256的unicode_latin1静态数组中。否则会为该字符调用PyUnicode_New函数分配内存并缓存到unicode_latin1数组后再返回。
static PyObject*
get_latin1_char(unsigned char ch)
{
PyObject *unicode;
#ifdef LATIN1_SINGLETONS
unicode = unicode_latin1[ch];
//如果该字符已缓存在unicode_latin1中,立即返回
if (unicode) {
Py_INCREF(unicode);
return unicode;
}
#endif
//否则会为该字符分配内存
unicode = PyUnicode_New(1, ch);
if (!unicode) {
return NULL;
}
PyUnicode_1BYTE_DATA(unicode)[0] = ch;
assert(_PyUnicode_CheckConsistency(unicode, 1));
#ifdef LATIN1_SINGLETONS
Py_INCREF(unicode);
unicode_latin1[ch] = unicode;
#endif
return unicode;
}
第2种情况:假定字符串长度不超过127,即由ASCII区间内的任意编码组成的字符串
这一逻辑推定的事实是前127个字符编码(即ASCII字符集)是unicode字符集的一个子集。不论传入的C级别字符串属于哪一种情况,都需经过一个特殊的ascii_decode函数,这个ascii_decode函数对于在如下情况通常给unicode_decode_utf8函数返回0的偏移量
- 纯ASCII字符串或纯中文字符的unicode字符串
- 任意ASCII字符和多国unicode字符编码混合的字符串
PS:具体的源代码请查看下面代码,关于该函数CPython源代码文档,以及官方网站的API说明都没有提及,因此,我对其算法甚少理解,有大伙提供详细信息,烦请跟帖评论留言。
static Py_ssize_t
ascii_decode(const char *start, const char *end, Py_UCS1 *dest)
{
const char *p = start;
const char *aligned_end = (const char *) _Py_ALIGN_DOWN(end, SIZEOF_LONG);
#if !defined(__m68k__)
#if SIZEOF_LONG <= SIZEOF_VOID_P
//断言dest是按8字节对齐
assert(_Py_IS_ALIGNED(dest, SIZEOF_LONG));
if (_Py_IS_ALIGNED(p, SIZEOF_LONG)) {
/* Fast path, see in STRINGLIB(utf8_decode) for
an explanation. */
/* Help allocation */
const char *_p = p;
Py_UCS1 * q = dest;
while (_p < aligned_end) {
unsigned long value = *(const unsigned long *) _p;
if (value & ASCII_CHAR_MASK)
break;
*((unsigned long *)q) = value;
_p += SIZEOF_LONG;
q += SIZEOF_LONG;
}
p = _p;
while (p < end) {
if ((unsigned char)*p & 0x80)
break;
*q++ = *p++;
}
return p - start;
}
#endif
#endif
while (p < end) {
/* Fast path, see in STRINGLIB(utf8_decode) in stringlib/codecs.h
for an explanation. */
if (_Py_IS_ALIGNED(p, SIZEOF_LONG)) {
/* Help allocation */
const char *_p = p;
while (_p < aligned_end) {
unsigned long value = *(const unsigned long *) _p;
if (value & ASCII_CHAR_MASK)
break;
_p += SIZEOF_LONG;
}
p = _p;
if (_p == end)
break;
}
if ((unsigned char)*p & 0x80)
break;
++p;
}
memcpy(dest, start, p - start);
return p - start;
}
我们上面示例字符串在初始化时过程前,我们在其C函数内用pinrtf函数的关键信息的输出,编译后运行如下图
![1004d4ca8bc46dc252a4157691b20227.png](https://i-blog.csdnimg.cn/blog_migrate/6fac287d96ad7f35e63ed2a40ba4a90d.jpeg)
我们将上面的信息绘制成一个内存图,自然就一目了然啦。由于ascii_decode在函数返回后,对于任意的ASCII字符串对象或纯Unicode编码的字符串对象,p-start的偏移量始终为0.
![df7e74e725432ca8053da575671a96e3.png](https://i-blog.csdnimg.cn/blog_migrate/30977ab140bc8a4aa2e49d9c9e3e2a37.png)
ss8..png
还有更多的细节,我们说本实例的字符串的长度是29字节,前27个字节是unicode编码,而最后两个字节是纯粹ASCII字符。其实UTF-8的思想是使用不同长度的字节序列对各种Unicode字符进行编码, 标准的ASCII字符,即包括拉丁字母数字和标点符号使用一个字节、ASCII扩展字符都以2字节的顺序排列、 韩文,中文和日文表意文字使用3字节序列。
小结
我们本篇讨论了字符串对象的内存分配PyUnicode_New函数,以及提出了CPython3.3+的字符串初始化的函数调用路径,先讨论了unicode_decode_utf8函数和ascii_decode函数的一些细节问题。下一篇会讨论剩下的unicode_decode_utf8代码细节