本文我们将讨论字符串是如何被创建的,首先CPython提供了很多的特型API用于字符串的创建,我们一起来看一下。
PyUnicode_FromString
最简单的一个特型API,根据 C 的char *来创建Python的unicode PyObject * PyUnicode_FromString(const char *u) { // 计算C字符串的长度 size_t size = strlen(u); // 超过了PY_SSIZE_T_MAX,报错 if (size > PY_SSIZE_T_MAX) { PyErr_SetString(PyExc_OverflowError, "input too long"); return NULL; } // 调用该函数进行创建,这个函数后面介绍 return PyUnicode_DecodeUTF8Stateful(u, (Py_ssize_t)size, NULL, NULL); }
所以该函数就是根据C的字符串创建Python的字符串,并且从源码中可以看到Python的字符串是由长度限制的,不能超过一个long所表示的最大范围。 PyUnicode_FromStringAndSize
也是根据 C 的char *来创建Python的unicode,但不同的是可以指定一个长度。而在PyUnicode_FromString里面,长度写死的,就是 C 字符串的长度。 PyObject * PyUnicode_FromStringAndSize(const char *u, Py_ssize_t size) { if (size < 0) { PyErr_SetString(PyExc_SystemError, "Negative size passed to PyUnicode_FromStringAndSize"); return NULL; } // 如果C的char *不为NULL,那么还是调用这个函数创建 if (u != NULL) return PyUnicode_DecodeUTF8Stateful(u, size, NULL, NULL); else // 否则的话,直接调用 _PyUnicode_New 申请对应大小的空间 return (PyObject *)_PyUnicode_New(size); }
当然还有很多其它的创建方式,比如PyUnicode_FromUnicode,根据wchar_t *创建Python字符串。
这里代码我们就不看了,我们使用 Cython 演示一下。
from cpython.unicode cimport PyUnicode_FromUnicode
# Py_UNICODE 理解为 C 的宽字符即可 cdef Py_UNICODE* s = "古明地觉的 Python小屋" # 根据宽字符创建字符串,当然也可以指定字符数量 # 这里我们只选择前 4 个宽字符 cdef str name = PyUnicode_FromUnicode(s, 4) print(name) # 古明地觉
然后我们重点看一下PyUnicode_DecodeUTF8Stateful这个函数。 PyUnicode_DecodeUTF8Stateful
先来说一下大致的流程,想象一下我们在Python里面创建一个字符串,比如:s="你好",那么这个过程在 C 中就等价于将这个字符串字面量(const char* 指向)拷贝到 PyUnicode_New 所申请的堆内存中。
以上便是字符串的初始化,而字符串的初始化是以PyUnicode_DecodeUTF8Stateful函数为起点的,这个函数会在内部调用unicode_decode_utf8函数。 PyObject * PyUnicode_DecodeUTF8Stateful(const char *s, Py_ssize_t size, const char *errors, Py_ssize_t *consumed) { return unicode_decode_utf8(s, size, _Py_ERROR_UNKNOWN, errors, consumed); }
在unicode_decode_utf8里面会直接创建PyASCIIObject,因为解释器会假设你创建的是纯ASCII字符串,然后再调用ascii_decode进行判断,如果成功则直接返回,这在程序中属于快分支。
但如果我们创建的不是纯ASCII字符串,那么会判断这个字符串到底属于 PyUnicode_1BYTE_KIND、PyUnicode_2BYTE_KIND、PyUnicode_4BYTE_KIND 三者的哪一种,然后保存在 writer 的kind 成员中。 然后根据不同的类型调用不同的方法,比如:ucs1lib_utf8_decode、ucs2lib_utf8_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) { _PyUnicodeWriter writer; //直接使用 ascii_decode 进行解码 //因为会假设是一个ASCII字符串 writer.pos = ascii_decode(s, end, writer.data); s += writer.pos; //逐个字符进行扫描 while (s < end) { //然后获取 kind Py_UCS4 ch; int kind = writer.kind; //判断kind到底是哪一种 //不同的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); } ...... ...... }
所以能看出创建字符串的过程就是将char * 指向的字节序列进行解码、然后拷贝的一个过程,因为解释器会假设char *指向的是一个简单的 ASCII 字节序列。 然后按照PyUnicode_1BYTE_KIND的模式去计算字符串所需的堆内存空间,而后续的字节编码检测算法发现该C级别的字节序列出现了maxchar大于255的字符,就会尝试以PyUnicode_2BYTE_KIND的模式,再次重新计算内存空间并调用malloc分配函数。 因此所以创建一个字符串,涉及 1 到 3 次的 malloc,效率还是比较低下的。 以上就是字符串的创建,事实上字符串还是很复杂的,它并没有我们想象的那么简单。
|