《python解释器源码剖析》第3章--python中的字符串对象
3.0 序
我们知道python中的字符串属于变长对象,当然和int也是一样,底层的结构体实例所维护的数据的长度,在对象没有定义的时候是不知道的。当然如果是python2的话,底层PyIntObject维护的就是一个long,显然在没创建的时候就知道是1。
可变对象维护的数据的长度只能在对象创建的时候才能确定,举个例子,我们只能在创建一个字符串或者列表时,才知道它们所维护的数据的长度,在此之前,我们对此是一无所知的。
注意我们在前面提到过可变对象和不可变对象的区别,在变长对象中,实际上也可以分为可变对象和不可变对象。list和str实例化之后都是变长对象,但是list实例所维护数据是可以动态变化的,但是str实例就不支持添加、删除等操作了。下面我们来研究一下python变长对象中的不可变对象。
3.1 PyUnicodeObject和PyObject_Type
在Python中,PyUnicodeObject是对字符串对象的实现。PyUnicodeObject是一个拥有可变长度内存的对象,这一点很好理解。因为对于表示"hi"和"satori"的两个不同的PyUnicodeObject对象,其内部所需要保存字符串(或者说n个char)的内存空间显然是不一样的。与此同时,PyUnicodeObject又是一个不可变对象,一旦创建之后,内部维护的数据就不可以再修改了。这一特性使得PyUnicodeObject对象可以作为dict的key;但与此同时,当进行多个字符串连接等操作时,也会使效率大大降低。
我们看看PyUnicodeObject的定义:
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 compact:1; unsigned int ascii:1; unsigned int ready:1; unsigned int :24; } state; wchar_t *wstr; /* wchar_t representation (null-terminated) */ } PyASCIIObject;
可以看到PyUnicodeObject实现起来很复杂,这是因为在python中,默认都是Unicode。直接分析起来很费劲,我们可以阅读一篇文章,来看看python在存储字符串的时候是如何节省内存的,从而进一步认识PyUnicodeObject。链接如下:https://rushter.com/blog/python-strings-and-memory/
,这里我给翻译一下。
python在存储字符串的时候如何节省内存
从python3开始,str类型使用的是Unicode。而根据编码的不同,Unicode的每个字符最大可以占到4字节,从内存的角度来说, 这种编码有时会比较昂贵
为了减少内存消耗并且提高性能,python的内部使用了三种方式表示Unicode
- 每个字符一字节(Latin-1 编码)
- 每个字符二字节(UCS-2 编码)
- 每个字符四字节(UCS-4 编码)
在python编程中,所有字符串行为都是一致的,而且大多数时间我们都没有注意到差异。然而在处理大文本的时候,这种差异就会变得异常显著、甚至有些让人出乎意料
为了看到内部表示的差异,我们使用
sys.getsizeof
函数,返回一个对象所占的字节数# -*- coding:utf-8 -*- # @Author: WanMingZhu # @Date: 2019/10/25 14:01 import sys string = "hello" print(sys.getsizeof(string)) # 54 # 1 bytes print(sys.getsizeof(string + "!") - sys.getsizeof(string)) # 1 string2 = "你" # 2 bytes print(sys.getsizeof(string2 + "好") - sys.getsizeof(string2)) # 2 print(sys.getsizeof(string2)) # 76 string3 = "??" print(sys.getsizeof(string3 + "??") - sys.getsizeof(string3)) # 4正如你所见,python面对不同的字符会采用不同的编码。需要注意的是,python中的每一个string都需要额外的占用49-80字节,因为要存储一些额外信息,比如:哈希、长度、字节长度、编码类型等等。这也是为什么一个空字符串要占49个字节。
如果字符串中的所有字符都在ASCII范围内,则使用1字节Latin-1对其进行编码。基本上,Latin-1能表示前256个Unicode字符。它支持多种拉丁语,如英语、瑞典语、意大利语、挪威语。但是它们不能存储非拉丁语言,比如汉语、日语、希伯来语、西里尔语。这是因为它们的代码点(数字索引)定义在1字节(0-255)范围之外。
print(ord('a')) # 97 print(ord('你')) # 20320 print(ord('!')) # 33大多数流行的自然语言都可以采用2字节(UCS-2)编码。当字符串包含特殊符号、emoji或稀有语言时,使用4字节(UCS-4)编码。Unicode标准有将近300个块(范围)。你可以在0XFFFF块之后找到4字节块。假设我们有一个10G的ASCII文本,我们想把它加载到内存中,但如果我们在文本中插入一个表情符号,那么字符串的大小将增加4倍。这是一个巨大的差异,你可能会在实践当中遇到,比如处理NLP问题。
# -*- coding:utf-8 -*- # @Author: WanMingZhu # @Date: 2019/10/25 14:01 import sys string1 = "hello" string2 = "你" # 此时的string1,一个字符一个字节 print(sys.getsizeof(string1) - sys.getsizeof("")) # 5 # 此时变成10个字节了 print(sys.getsizeof(string1 + string2) - sys.getsizeof(string2)) # 10 """ 首先python3中,字符串是使用Unicode 对于string1来说,显然是使用1字节的Latin 1就可以存储。 但是一旦和string2组合,那么Latin 1是没办法存储的,因此会采用UCS-2存储,因此每个字符就变成了2字节 因此会比之前多5个字节。 不过可能有人好奇,不是说中文占3个字节,英文占1个字节吗。 那是字符串在使用utf-8编码成字节之后所占的大小。至于字符串本身, 就是我们所的那三个Latin 1 、UCS-2、UCS-4,分别占1、2、4个字节 """为什么python内部不使用utf-8
最著名和流行的Unicode编码都是utf-8,但是python不在内部使用它。
当一个字符串使用utf-8编码存储时,根据它所表示的字符,每个字符使用使用1-4个字节进行编码。这是一种存储效率很高的编码,但是它有一个明显的缺点。由于每个字符的字节长度可能不同,因此如果没有扫描字符串的方法,就无法按照索引随机访问单个字符。因此要对使用utf-8编码的字符串执行一个简单的操作,比如string[5],就意味着python需要扫描每一个字符,直到找到需要的字符,这样效率是很低的。但如果是固定长度的编码就没有这样的问题,python只需要将索引乘上一个字符所占的长度(1、2或者4),就可以瞬间定位到某一个字符。所以我们刚才看到,当Latin 1存储的hello,再和‘你‘这个汉字组合之后,整体每一个字符都会向大的方向扩展、变成了2字节。这样定位字符的时候,只需要将索引成上2即可。但如果原来的‘hello‘还是一个字节、而汉字是2字节,那么只通过索引是不可能定位到准确字符的,因为不同类型字符的编码不同,必须要扫描整个字符串才可以。但是扫描字符串,效率又比较低。所以python内部才会使用这个方法,而不是使用utf-8。
字符串intern机制
python中的,ASCII字符串,如果长度没有超过20个。那么不管创建多少个这样的对象,内存中只会有一份。
a = "mashiro" b = "satori" print(a[-1], b[-3]) # o o print(a[-1] is b[-3]) # True如你所见,两个相同的字符的只有一份,这是因为python中的字符串是不变的。在python中,不限于单个字符、或者空字符串,如果代码在编译期间创建的ASCII字符串的长度不超过20个,那么也会执行intern机制,这包括:
- 函数和类名
- 变量名
- 参数名
- 常量(代码中定义的所有字符串)
- 字典的key
- 属性名称
字符串intern机制省去了数万个重复的字符串分配。字符串内部是由全局字典维护的,其中字符串作为key。为了检查内存中是否已经有一个相同的字符串,python会执行字典的成员操作。而且我们所有的对象都有一个自己的属性字典,因此字典这个数据结构在python中是经过高度优化的。
再回到PyUnicodeObject,我们注意到里面有一个 wstr_length,它保存着对象中维护的可变长度内存的大小。wchar_t *wstr,能够看出这是一个与字符指针有关的指针,它指向一段内存,而这段内存保存着这个字符串对象所维护的实际字符串。显然这段内存不会只有一个字节,这段内存的实际长度是由 wstr_length来维护的,这个机制是python中所有变长对象的实现机制。比如:"satori",这个字符串,对应的底层PyUnicodeObject对象的 wstr_length就是6。
同C语言中的字符串一样,PyUnicodeObject内部维护的字符串在末尾必须以‘\0‘结尾,但是由于字符串的实际上度是由length来维护的,所以PyUnicodeObject表示字符串对象中间是可以出现‘\0‘的,这一点与C语言不同。因为在C中,只要遇到了字符串‘\0‘,就认为一个字符串结束了。所以实际上,wchar_t *wstr指向的是一段长度为length+1的个字节的内存,而且必须满足wstr[length] ==?‘\0‘
Py_hash_t hash,这个变量是用于缓存该对象的hash值,这样可以避免每一次都重新计算该字符串的hash值。如果一个PyUnicodeObject还没有被计算hash值,那么初始值就是-1。以后在剖析dict时,就会看到这个hash值将发挥巨大的作用。计算一个字符串对象的hash值,将会采用如下的算法:
static Py_hash_t unicode_hash(PyObject *self) { Py_ssize_t len; Py_uhash_t x; /* Unsigned for defined overflow behavior. */ #ifdef Py_DEBUG assert(_Py_HashSecret_Initialized); #endif if (_PyUnicode_HASH(self) != -1) return _PyUnicode_HASH(self); if (PyUnicode_READY(self) == -1) return -1; len = PyUnicode_GET_LENGTH(self); /* We make the hash of the empty string be 0, rather than using (prefix ^ suffix), since this slightly obfuscates the hash secret */ if (len == 0) { _PyUnicode_HASH(self) = 0; return 0; } x = _Py_HashBytes(PyUnicode_DATA(self), PyUnicode_GET_LENGTH(self) * PyUnicode_KIND(self)); _PyUnicode_HASH(self) = x; return x; }
再来看看PyUnicodeObject对应的类型PyUnicode_Type
PyTypeObject PyUnicode_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "str", /* tp_name */ sizeof(PyUnicodeObject), /* tp_size */ 0, /* tp_itemsize */ /* Slots */ (destructor)unicode_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 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, /* 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 */ };
而且我们注意到,tp_as_number,tp_as_sequence,tp_as_mapping三个域都被设置了。这表示PyUnicodeObject对数值操作、序列操作和映射操作都支持。
3.2 创建PyUnicodeObject对象
python提供了两条路径,从C中原生的字符串创建PyUnicodeObject对象,我们先看看最一般的PyUnicode_FromString
PyUnicode_FromString(const char *u) { size_t size = strlen(u); // PY_SSIZE_T_MAX是一个与平台相关的数值,在64位系统下是4GB //如果创建的字符串的长度超过了这个值,那么会报错 //个人觉得这种情况应该不会发生,就跟变量的引用计数一样 //只要不是吃饱了撑的,写恶意代码,基本不会超过这个阈值 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); }
另一种创建的方式就是PyUnicode_FromUnicodeAndSize
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; } if (u != NULL) return PyUnicode_DecodeUTF8Stateful(u, size, NULL, NULL); else return (PyObject *)_PyUnicode_New(size); }
这两者的操作上基本是一致的,只不过第一种方式要求传入的参数必须是以‘\0‘结尾的字符数组指针,而第二种方式则没有此要求。因为我们发现后者多了一个参数size,因为通过size就可以确定需要拷贝的字符个数。
3.3 字符串对象的intern机制
在python中,也有像小整数一样将段字符串作为共享其他变量引用,以达到节省内存和性能上不必要的开销,这就是intern机制:
void PyUnicode_InternInPlace(PyObject **p) { PyObject *s = *p; PyObject *t; #ifdef Py_DEBUG assert(s != NULL); assert(_PyUnicode_CHECK(s)); #else if (s == NULL || !PyUnicode_Check(s)) return; #endif //对PyUnicodeObjec进行类型和状态检查 if (!PyUnicode_CheckExact(s)) return; //检测intern机制 if (PyUnicode_CHECK_INTERNED(s)) return; //创建intern机制的dict if (interned == NULL) { interned = PyDict_New(); if (interned == NULL) { PyErr_Clear(); /* Don't leave an exception */ return; } } Py_ALLOW_RECURSION //判断对象是否存在于字典中 t = PyDict_SetDefault(interned, s, s); Py_END_ALLOW_RECURSION if (t == NULL) { PyErr_Clear(); return; } //存在的话,调整引用计数 if (t != s) { Py_INCREF(t); Py_SETREF(*p, t); return; } /* The two references in interned are not counted by refcnt. The deallocator will take care of this */ Py_REFCNT(s) -= 2; _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL; }
在PyDict_SetDefault
函数中首先会进行一系列的检查,包括类型检查、因为intern共享机制只能用在字符串对象上。检查传入的对象是否已经被intern机制处理过了
我们在代码中看到了interned = PyDict_New()
,这个PyDict_New()
是python中的dict对象,因此可以发现,就是在程序中有一个key、value映射关系的集合。
intern机制中的PyUnicodObject采用了特殊的引用计数机制。将一个PyUnicodeObject对象a的PyObject指针作为key和valu添加到intered中时,PyDictObjec对象会通过这两个指针对a的引用计数进行两次+1操作。这会造成a的引用计数在python程序结束前永远不会为0,这也是 Py_REFCNT(s) -= 2;
将计数减2的原因。
python在创建一个字符串时,会首先检测是否已经有该字符串对应的PyUnicodeObject对象了,如果有,就不用创建新的,这样可以节省空间。但其实不是这样的,事实上,节省内存空间是没错的,可python并不是在创建PyUnicodeObject的时候就通过intern机制实现了节省空间的目的。从PyUnicode_FromString中我们可以看到,无论如何一个合法的PyUnicodeObject总是会被创建的,而intern机制也只对PyUnicodeObject起作用。
对于任何一个字符串string,python总是会为它创建对应的PyUnicodeObject,尽管创建出来的对象所维护的字符数组,在intern机制中已经存在了(有另外的PyUnicodeObject也维护了相同的字符数组)。而这正是关键所在,通常python在运行时创建了一个PyUnicodeObject对象temp之后,基本上都会调用PyUnicode_InternInPlace对temp进行处理,如果维护的字符数组有其他的PyUnicodeObject维护了,或者说其他的PyUnicodeObject对象维护了一个与之一模一样的字符数组,那么temp的引用计数就会减去1。temp由于引用计数为0而被销毁,只是昙花一现,然后归于湮灭。
所以现在我们就明白了intern机制,并不是说先判断是否存在,如果存在,就不创建。而是先创建,然后发现已经有其他的PyUnicodeObject维护了一个与之相同的字符数组,于是intern机制将引用计数减一,导致引用计数为0,最终被回收。
但是这么做的原因是什么呢?为什么非要创建一个PyUnicodeObject来完成intern操作呢?这是因为PyDictObject必须要以PyObject *作为key
关于PyUnicodeObject对象的intern机制,还有一点需要注意。实际上,被intern机制处理过后的字符串分为两类,一类处于SSTATE_INTERNED_IMMORTAL
,另一类处于SSTATE_INTERNED_MORTAL
状态,
这两种状态的区别在unicode_dealloc
中可以清晰的看到,SSTATE_INTERNED_IMMORTAL
状态的PyUnicodeObject是永远不会被销毁的,它与python解释器共存亡。
PyUnicode_InternInPlace只能创建SSTATE_INTERNED_MORTAL
的PyUnicodeObject对象,如果想创建SSTATE_INTERNED_IMMORTAL
对象,必须通过另外的接口来强制改变PyUnicodeObject的intern状态
void PyUnicode_InternImmortal(PyObject **p) { PyUnicode_InternInPlace(p); if (PyUnicode_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) { _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL; Py_INCREF(*p); } }
3.4 字符缓冲池
正如整数有小整数对象池,字符串,也有对应的PyUnicodeObject对象池。
在python中的整数对象中,小整数的缓冲池是在python初始化的时候被创建的,而字符串对象体系中的字符缓冲池则是以静态变量的形式存在的。在python初始化完成之后,缓冲池的所有PyUnicodeObject指针都为空。
当创建一个PyUnicodeObject对象时,如果字符串实际上是一个字符。那么会先对字符对象进行intern操作,再将intern的结果缓存到字符缓冲池当中。同样当再次创建PyUnicodeObject对象时,检测维护的是不是只有一个字符,然后检查字符是不是存在于缓冲池中,如果存在,直接返回
3.5 PyUnicodeObject效率相关问题
关于PyUnicodeObject,有一个极大影响效率的问题。假设现在有两个字符串,对于java、c#,go、python等语言,我们都可以使用+将两者拼接起来。但是在python中,这种做法正是导致效率低下的万恶之源。
从之前的学习中,我们也知道了,PyUnicodeObject是一个不可变对象,这就意味着当两个PyUnicodeObject相加时,必须要创建新的PyUnicodeObject对象,维护的字符串是之前的两个对象维护的字符串的拼接。每两个PyUnicodeObject相加就要创建一个新的,那如果n个PyUnicodeObject相加,就意味着要创建n-1个PyUnicodeObject对象,而且创建了还需要在销毁。毫无疑问,这极大地影响了python的效率。
PyObject * PyUnicode_Concat(PyObject *left, PyObject *right) { PyObject *result; Py_UCS4 maxchar, maxchar2; Py_ssize_t left_len, right_len, new_len; if (ensure_unicode(left) < 0) return NULL; if (!PyUnicode_Check(right)) { PyErr_Format(PyExc_TypeError, "can only concatenate str (not \"%.200s\") to str", right->ob_type->tp_name); return NULL; } if (PyUnicode_READY(right) < 0) return NULL; /* Shortcuts */ if (left == unicode_empty) return PyUnicode_FromObject(right); if (right == unicode_empty) return PyUnicode_FromObject(left); //获取两个PyUnicodeObject对象的长度 left_len = PyUnicode_GET_LENGTH(left); right_len = PyUnicode_GET_LENGTH(right); if (left_len > PY_SSIZE_T_MAX - right_len) { PyErr_SetString(PyExc_OverflowError, "strings are too large to concat"); return NULL; } //相加作为新的PyUnicodeObject对象的长度 new_len = left_len + right_len; maxchar = PyUnicode_MAX_CHAR_VALUE(left); maxchar2 = PyUnicode_MAX_CHAR_VALUE(right); maxchar = Py_MAX(maxchar, maxchar2); /* Concat the two Unicode strings */ //声明一个新的PyUnicodeObject对象 result = PyUnicode_New(new_len, maxchar); if (result == NULL) return NULL; _PyUnicode_FastCopyCharacters(result, 0, left, 0, left_len); _PyUnicode_FastCopyCharacters(result, left_len, right, 0, right_len); assert(_PyUnicode_CheckConsistency(result, 1)); return result; }
官方推荐的做法是,将n的PyUnicodeObject对象放在list或者tuple中,然后使用PyUnicodeObject的join操作,这样的话只需要分配一次内存,执行效率大大提高。
PyObject * PyUnicode_Join(PyObject *separator, PyObject *seq) { PyObject *res; PyObject *fseq; Py_ssize_t seqlen; PyObject **items; fseq = PySequence_Fast(seq, "can only join an iterable"); if (fseq == NULL) { return NULL; } /* NOTE: the following code can't call back into Python code, * so we are sure that fseq won't be mutated. */ items = PySequence_Fast_ITEMS(fseq); seqlen = PySequence_Fast_GET_SIZE(fseq); res = _PyUnicode_JoinArray(separator, items, seqlen); Py_DECREF(fseq); return res; } PyObject * _PyUnicode_JoinArray(PyObject *separator, PyObject *const *items, Py_ssize_t seqlen) { PyObject *res = NULL; /* the result */ PyObject *sep = NULL; Py_ssize_t seplen; PyObject *item; Py_ssize_t sz, i, res_offset; Py_UCS4 maxchar; Py_UCS4 item_maxchar; int use_memcpy; unsigned char *res_data = NULL, *sep_data = NULL; PyObject *last_obj; unsigned int kind = 0; /* If empty sequence, return u"". */ if (seqlen == 0) { _Py_RETURN_UNICODE_EMPTY(); } /* If singleton sequence with an exact Unicode, return that. */ last_obj = NULL; if (seqlen == 1) { if (PyUnicode_CheckExact(items[0])) { res = items[0]; Py_INCREF(res); return res; } seplen = 0; maxchar = 0; } else { /* Set up sep and seplen */ if (separator == NULL) { /* fall back to a blank space separator */ sep = PyUnicode_FromOrdinal(' '); if (!sep) goto onError; seplen = 1; maxchar = 32; } else { if (!PyUnicode_Check(separator)) { PyErr_Format(PyExc_TypeError, "separator: expected str instance," " %.80s found", Py_TYPE(separator)->tp_name); goto onError; } if (PyUnicode_READY(separator)) goto onError; sep = separator; seplen = PyUnicode_GET_LENGTH(separator); maxchar = PyUnicode_MAX_CHAR_VALUE(separator); /* inc refcount to keep this code path symmetric with the above case of a blank separator */ Py_INCREF(sep); } last_obj = sep; } /* There are at least two things to join, or else we have a subclass * of str in the sequence. * Do a pre-pass to figure out the total amount of space we'll * need (sz), and see whether all argument are strings. */ sz = 0; #ifdef Py_DEBUG use_memcpy = 0; #else use_memcpy = 1; #endif for (i = 0; i < seqlen; i++) { size_t add_sz; item = items[i]; if (!PyUnicode_Check(item)) { PyErr_Format(PyExc_TypeError, "sequence item %zd: expected str instance," " %.80s found", i, Py_TYPE(item)->tp_name); goto onError; } if (PyUnicode_READY(item) == -1) goto onError; add_sz = PyUnicode_GET_LENGTH(item); item_maxchar = PyUnicode_MAX_CHAR_VALUE(item); maxchar = Py_MAX(maxchar, item_maxchar); if (i != 0) { add_sz += seplen; } if (add_sz > (size_t)(PY_SSIZE_T_MAX - sz)) { PyErr_SetString(PyExc_OverflowError, "join() result is too long for a Python string"); goto onError; } sz += add_sz; if (use_memcpy && last_obj != NULL) { if (PyUnicode_KIND(last_obj) != PyUnicode_KIND(item)) use_memcpy = 0; } last_obj = item; } res = PyUnicode_New(sz, maxchar); if (res == NULL) goto onError; /* Catenate everything. */ #ifdef Py_DEBUG use_memcpy = 0; #else if (use_memcpy) { res_data = PyUnicode_1BYTE_DATA(res); kind = PyUnicode_KIND(res); if (seplen != 0) sep_data = PyUnicode_1BYTE_DATA(sep); } #endif if (use_memcpy) { for (i = 0; i < seqlen; ++i) { Py_ssize_t itemlen; item = items[i]; /* Copy item, and maybe the separator. */ if (i && seplen != 0) { memcpy(res_data, sep_data, kind * seplen); res_data += kind * seplen; } itemlen = PyUnicode_GET_LENGTH(item); if (itemlen != 0) { memcpy(res_data, PyUnicode_DATA(item), kind * itemlen); res_data += kind * itemlen; } } assert(res_data == PyUnicode_1BYTE_DATA(res) + kind * PyUnicode_GET_LENGTH(res)); } else { for (i = 0, res_offset = 0; i < seqlen; ++i) { Py_ssize_t itemlen; item = items[i]; /* Copy item, and maybe the separator. */ if (i && seplen != 0) { _PyUnicode_FastCopyCharacters(res, res_offset, sep, 0, seplen); res_offset += seplen; } itemlen = PyUnicode_GET_LENGTH(item); if (itemlen != 0) { _PyUnicode_FastCopyCharacters(res, res_offset, item, 0, itemlen); res_offset += itemlen; } } assert(res_offset == PyUnicode_GET_LENGTH(res)); } Py_XDECREF(sep); assert(_PyUnicode_CheckConsistency(res, 1)); return res; onError: Py_XDECREF(sep); Py_XDECREF(res); return NULL; }
执行join操作时,首先会统计list中多少个PyUnicodeObject对象,并统计每个对象所维护的字符串有多长, 进行求和执行一次申请空间。再逐一进行字符串拷贝。