《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虚拟机中的控制流

《python解释器源码剖析》第16章--python的多线程机制

《python解释器源码剖析》第15章--python模块的动态加载机制