> 文章列表 > 【通过Cpython3.9源码看看python字符串对象的创建】

【通过Cpython3.9源码看看python字符串对象的创建】

【通过Cpython3.9源码看看python字符串对象的创建】

在这里插入图片描述

CPython源码解析之PyUnicode_New函数实现

简介

PyUnicode_New是Python源码中用于创建Unicode字符串对象的函数,定义在UnicodeObject.c文件中。该函数接受一个长度参数size和最大字符值参数maxchar(根据传入的最大字符值 maxchar 确定新创建的字符串对象所需的存储类型和大小),并返回一个新的Unicode字符串对象。

该函数根据maxchar的值判断Unicode字符串对象的编码类型**(字符串对象有三种可能的存储格式:1 字节(byte)、2 字节(UCS-2)和 4 字节(UCS-4))**,并根据size和字符大小分配内存。同时,如果size为0,则会返回预先分配好的空字符串对象unicode_empty,从而进行一些优化。

在函数实现中,还会根据字符大小和maxchar的值来设置对象的一些属性(如kindchar_size等),同时进行内存分配和初始化等操作。

函数实现

函数实现的主要流程如下:

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;/* Optimization for empty strings */if (size == 0 && unicode_empty != NULL) {Py_INCREF(unicode_empty);return unicode_empty;}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);}else if (maxchar < 256) {kind = PyUnicode_1BYTE_KIND;char_size = 1;}else if (maxchar < 65536) {kind = PyUnicode_2BYTE_KIND;char_size = 2;if (sizeof(wchar_t) == 2)is_sharing = 1;}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();/* Duplicated allocation code from _PyObject_New() instead of a call to* PyObject_New() so we are able to allocate space for the object and* it's data buffer.*/obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);if (obj == NULL)return PyErr_NoMemory();obj = PyObject_INIT(obj, &PyUnicode_Type);if (obj == NULL)return NULL;unicode = (PyCompactUnicodeObject *)obj;if (is_ascii)data = ((PyASCIIObject*)obj) + 1;
  1. 如果 maxchar 小于 128,表明字符串中所有字符都是 ASCII 字符,使用 PyUnicode_1BYTE_KIND 类型,每个字符占用 1 字节。此外,设置 is_ascii 为 1,并将 struct_size 设置为 PyASCIIObject 的大小。
  2. 如果 maxchar 小于 256,表明字符串中的所有字符都可以用一个字节表示,使用 PyUnicode_1BYTE_KIND 类型,每个字符占用 1 字节。
  3. 如果 maxchar 小于 65536,表明字符串中的字符可以用两个字节表示(UCS-2 编码),使用 PyUnicode_2BYTE_KIND 类型,每个字符占用 2 字节。同时,如果系统的 wchar_t 大小为 2 字节,设置 is_sharing 为 1,表示字符串对象和系统宽字符(wchar_t)可以共享内存。
  4. 如果 maxchar 大于等于 65536 且小于等于 MAX_UNICODE,表明字符串中的字符需要 4 个字节表示(UCS-4 编码),使用 PyUnicode_4BYTE_KIND 类型,每个字符占用 4 字节。同时,如果系统的 wchar_t 大小为 4 字节,设置 is_sharing 为 1,表示字符串对象和系统宽字符(wchar_t)可以共享内存。

如果 maxchar 大于 MAX_UNICODE,则抛出一个系统错误,因为这是一个无效的最大字符值。

判断是否为空字符串

在函数中首先对空字符串进行了判断,如果size等于0并且全局变量unicode_empty不为空,则返回全局变量unicode_emptyunicode_empty是一个预先分配好的空字符串对象,用于避免重复创建空字符串对象,从而提高程序效率。

    /* Optimization for empty strings */if (size == 0 && unicode_empty != NULL) {Py_INCREF(unicode_empty);return unicode_empty;}

确定编码类型和字符大小

接下来是根据maxchar的值判断Unicode字符串对象的编码类型和字符大小。如果maxchar小于128,则说明所有字符都是ASCII字符,可以使用ASCII编码类型,并且字符大小为1字节。否则,如果maxchar小于256,则仍然使用1字节编码类型。如果maxchar小于65536,则使用2字节编码类型,并根据wchar_t的大小来判断是否需要共享缓冲区(即是否需要在对象之后分配缓冲区)。如果maxchar大于65536,则使用4字节编码类型。

    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);}else if (maxchar < 256) {kind = PyUnicode_1BYTE_KIND;char_size = 1;}else if (maxchar < 65536) {kind = PyUnicode_2BYTE_KIND;char_size = 2;if (sizeof(wchar_t) == 2)is_sharing = 1;}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;}

检查长度是否有效

接下来是对字符串长度的检查。如果size小于0或者内存分配大小超过PY_SSIZE_T_MAX,则返回异常对象。

    /* 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_MALLOC函数分配内存,并使用PyObject_INIT函数初始化对象。PyObject_INIT函数与PyObject_New类似,但它可以分配额外的空间来存储数据缓冲区。在函数中,还会设置对象的一些属性(如_PyUnicode_LENGTH_PyUnicode_HASH等),并根据编码类型和字符大小分配数据缓冲区。如果使用ASCII编码类型,则缓冲区在对象之后分配;否则,缓冲区直接跟随对象分配。

详细分析下:

代码直接使用 PyObject_MALLOC 函数分配内存,而不是调用 PyObject_New 函数。这样做的原因是为了同时分配字符串对象及其数据缓冲区所需的内存。分配的内存大小为 struct_size(字符串对象的结构大小)加上所需的字符存储空间大小((size + 1) * char_size,其中 size 是字符串大小,char_size 是每个字符所需的字节数)。额外的 +1 是为了在数据缓冲区末尾预留一个空字节,用于存储空字符终止符。

如果内存分配失败,将返回内存不足的错误。成功分配内存后,使用 PyObject_INIT 函数初始化字符串对象,并将其类型设置为 PyUnicode_Type

接着,将 unicode 指针指向分配的内存空间,并根据字符串是否为 ASCII 类型确定数据缓冲区的位置。如果 is_ascii 为真,数据缓冲区紧跟在 PyASCIIObject 结构之后;否则,数据缓冲区紧跟在 PyCompactUnicodeObject 结构之后。将数据缓冲区的起始地址赋值给 data

然后就是设置了字符串对象(unicode)的各种属性:

  1. 设置字符串长度(_PyUnicode_LENGTH)为 size
  2. 初始化字符串的哈希值(_PyUnicode_HASH)为 -1(表示尚未计算哈希值)。
  3. 设置字符串对象的状态(_PyUnicode_STATE):
    • interned:表示字符串对象是否已被驻留(interned),初始值设为 0(表示未被驻留)。
    • kind:设置字符串的类型,即字符的存储方式(1字节、2字节或4字节)。
    • compact:设置为 1,表示使用紧凑的内存布局。
    • ready:设置为 1,表示字符串对象已准备好使用。
    • ascii:设置为 is_ascii 的值,表示字符串是否为 ASCII 字符串。
    obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);if (obj == NULL)return PyErr_NoMemory();obj = PyObject_INIT(obj, &PyUnicode_Type);if (obj == NULL)return NULL;unicode = (PyCompactUnicodeObject *)obj;if (is_ascii)data = ((PyASCIIObject*)obj) + 1;elsedata = unicode + 1;_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;

设置缓冲区

最后是根据编码类型和字符大小设置缓冲区。如果使用ASCII编码类型,则缓冲区的末尾需要添加一个空字符。如果使用1字节编码类型,则需要设置缓冲区的长度。如果使用2字节或4字节编码类型,则不需要设置长度。

  • 如果是 ASCII 字符串,将数据缓冲区的最后一个字节设置为 0(空字符终止符),并将 _PyUnicode_WSTR 设置为 NULL。
  • 如果是 1 字节非 ASCII 字符串,将数据缓冲区的最后一个字节设置为 0(空字符终止符),将 _PyUnicode_WSTR 设置为 NULL,将 _PyUnicode_WSTR_LENGTH 设置为 0,将 utf8 设置为 NULL,将 utf8_length 设置为 0。
  • 对于其他情况(2 字节或 4 字节 Unicode 字符串):
    • utf8 设置为 NULL,将 utf8_length 设置为 0。
    • 将数据缓冲区的最后一个字符设置为 0(空字符终止符)。
    • 如果 is_sharing 为真(表示 wchar_t 与字符串存储方式相同),将 _PyUnicode_WSTR_LENGTH 设置为 size,将 _PyUnicode_WSTR 设置为数据缓冲区的起始地址(data)。
    • 否则,将 _PyUnicode_WSTR_LENGTH 设置为 0,将 _PyUnicode_WSTR 设置为 NULL。
if (is_ascii) {((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;}
}

返回对象

最后,对对象进行一些检查,然后返回对象。在调试模式下,使用 unicode_fill_invalid 函数填充无效的 Unicode 字符串。最后,使用断言(assert)确保字符串对象的一致性。返回创建的字符串对象 obj

#ifdef Py_DEBUGunicode_fill_invalid((PyObject*)unicode, 0);
#endifassert(_PyUnicode_CheckConsistency((PyObject*)unicode, 0));return obj;
}
s1 = "Hello world!"    # ASCII编码类型
s2 = "你好,世界!"   # 3字节UTF-8编码类型
s3 = "🐍👍"           # 4字节UTF-8编码类型# 创建ASCII编码类型的Unicode字符串对象
u1 = PyUnicode_New(len(s1), 127)# 创建3字节UTF-8编码类型的Unicode字符串对象
u2 = PyUnicode_New(len(s2), 65535)创建4字节UTF-8编码类型的Unicode字符串对象
u3 = PyUnicode_New(len(s3), 1114111)在ASCII编码类型的Unicode字符串对象中添加数据
for i, c in enumerate(s1):PyUnicode_WRITE(u1, i, c)3字节UTF-8编码类型的Unicode字符串对象中添加数据
for i, c in enumerate(s2):PyUnicode_WRITE(u2, i, c)4字节UTF-8编码类型的Unicode字符串对象中添加数据
for i, c in enumerate(s3):PyUnicode_WRITE(u3, i, c)输出Unicode字符串对象的值
print(PyUnicode_AsWideCharString(u1, NULL)) # Hello world!
print(PyUnicode_AsWideCharString(u2, NULL)) # 你好,世界!
print(PyUnicode_AsWideCharString(u3, NULL)) # 🐍👍释放Unicode字符串对象的内存
Py_DECREF(u1)
Py_DECREF(u2)
Py_DECREF(u3)

上面的示例演示了如何使用PyUnicode_New函数创建不同编码类型的Unicode字符串对象,并使用PyUnicode_WRITE函数向对象中添加数据,最后使用PyUnicode_AsWideCharString函数输出Unicode字符串对象的值,并使用Py_DECREF函数释放Unicode字符串对象的内存。

需要注意的是,Python中有许多其他的字符串类型,如字节串(bytes)、字节数组(bytearray)和字符串(str)等。其中,字符串类型是Unicode字符串类型,它使用UTF-8编码类型存储Unicode字符。与PyUnicode_New函数对应的是PyUnicode_FromString函数,它可以从字符串中创建Unicode字符串对象。

补充说明

在Python中,字符串有两种类型,一种是字节串(bytes)类型,另一种是字符串(str)类型。字节串类型是一个字节序列,它以字节为单位存储数据。而字符串类型是一个Unicode字符序列,它以Unicode字符为单位存储数据,并使用UTF-8编码类型存储Unicode字符。在Python 3.x中,字符串类型是默认的字符串类型,而字节串类型则需要使用b前缀表示。例如:

# 字符串类型
s1 = "Hello, world!"# 字节串类型
s2 = b"Hello, world!"

PyUnicode_New函数是用于创建Unicode字符串对象的函数,可以指定字符串的长度和最大字符范围来创建不同类型的Unicode字符串对象。而PyUnicode_FromString函数则是从字符串中创建Unicode字符串对象的函数,它会自动检测字符串的编码类型,并创建对应的Unicode字符串对象。例如:

# 创建ASCII编码类型的Unicode字符串对象
u1 = PyUnicode_FromString("Hello, world!")# 创建UTF-8编码类型的Unicode字符串对象
u2 = PyUnicode_FromString("你好,世界!")# 创建UTF-16编码类型的Unicode字符串对象
u3 = PyUnicode_FromString("\\u4f60\\u597d\\uff0c\\u4e16\\u754c\\uff01")

PyUnicode_FromString函数会自动检测字符串的编码类型,并根据需要创建相应的Unicode字符串对象。例如,在上面的示例中,第一个字符串是使用ASCII编码类型的,因此创建的是ASCII编码类型的Unicode字符串对象;第二个字符串是使用UTF-8编码类型的,因此创建的是UTF-8编码类型的Unicode字符串对象;第三个字符串是使用UTF-16编码类型的,因此创建的是UTF-16编码类型的Unicode字符串对象。

需要注意的是,如果从字节串类型创建Unicode字符串对象,需要使用PyUnicode_Decode函数进行解码,以将字节串转换为Unicode字符串。例如:

# 创建UTF-8编码类型的字节串对象
b = b"\\xe4\\xbd\\xa0\\xe5\\xa5\\xbd\\xef\\xbc\\x8c\\xe4\\xb8\\x96\\xe7\\x95\\x8c\\xef\\xbc\\x81"# 将字节串对象解码为UTF-8编码类型的Unicode字符串对象
u = PyUnicode_Decode(b, strlen(b), "utf-8", "strict")

上面的示例中,我们首先创建了一个UTF-8编码类型的字节串对象,然后使用PyUnicode_Decode函数将其解码为UTF-8编码类型的Unicode字符串对象。