《python解释器源码剖析》第3章--python中的字符串对象
Posted 来自东方地灵殿的小提琴手
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《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 . */
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内部维护的字符串在末尾必须以‘ ‘结尾,但是由于字符串的实际上度是由length来维护的,所以PyUnicodeObject表示字符串对象中间是可以出现‘ ‘的,这一点与C语言不同。因为在C中,只要遇到了字符串‘ ‘,就认为一个字符串结束了。所以实际上,wchar_t *wstr指向的是一段长度为length+1的个字节的内存,而且必须满足wstr[length] ==?‘ ‘
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);
}
这两者的操作上基本是一致的,只不过第一种方式要求传入的参数必须是以‘ ‘结尾的字符数组指针,而第二种方式则没有此要求。因为我们发现后者多了一个参数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对象,并统计每个对象所维护的字符串有多长, 进行求和执行一次申请空间。再逐一进行字符串拷贝。
以上是关于《python解释器源码剖析》第3章--python中的字符串对象的主要内容,如果未能解决你的问题,请参考以下文章
《python解释器源码剖析》第4章--python中的list对象
《python解释器源码剖析》第13章--python虚拟机中的类机制
《python解释器源码剖析》第10章--python虚拟机中的一般表达式
《python解释器源码剖析》第11章--python虚拟机中的控制流