python的字符串对象

本文探讨Python3中的字符串对象,强调其采用的Unicode编码方式。内容涉及字符串对象的创建API、intern机制的工作原理,以及字符串缓冲池如何提高效率。文章还指出,对于大量字符串拼接,使用`join`比连续使用`+`更高效,以避免频繁的内存申请。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

python3中采用的是unicode编码方式

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 interned:2;
        unsigned int kind:3;
        unsigned int compact:1;
        unsigned int ascii:1;
        unsigned int ready:1;
        unsigned int :24;
    } state;
    wchar_t *wstr; 
} PyASCIIObject;

typedef struct {
    PyASCIIObject _base;
    Py_ssize_t utf8_length;
    char *utf8; 
    Py_ssize_t wstr_length; 
} PyCompactUnicodeObject;
typedef struct {
    PyCompactUnicodeObject _base;
    union {
        void *any;
        Py_UCS1 *latin1;
        Py_UCS2 *ucs2;
        Py_UCS4 *ucs4;
    } data;                     /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;

如何创建一个字符串对象,python提供了很多的API来创建一个字符串对象,这只是其中的一种

PyObject *PyUnicode_FromString(const char *u)
{
    size_t size = strlen(u);
    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);
}

PY_SSIZE_T_MAX是跟平台有关的值,在WIN64系统下数值应该为
4 294 967 295换算一下也就是4GB,而WIN32系统下则为2GB,一般我们是碰不到这个临界值的。

字符串对象的intern机制

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
    /* If it's a subclass, we don't really know what putting
       it in the interned dict might do. */
    if (!PyUnicode_CheckExact(s))
        return;
    if (PyUnicode_CHECK_INTERNED(s))
        return;
    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;
}

这里会进行两处检查,一次类型检查和一次状态检查。
类型检查:检查传入的对象是否时一个PyUnicodeObject对象,interb机制指挥应用在这个对象上
状态检查:检查传入的PyUnicodeObject对象是否已经被intern机制处理过了。
继续看代码,intern实际上指向的是PyDict_New创建的一个对象,实际上是创建了一个PyDictObject对象。
这个机制实际上就是,对传入的一个PyUnicodeObject对象s,python会在intern机制创建的interned集合中检查是否有满足以下条件的对象t:t中维护的原生字符串与s相同,如果存在,那么指向a的PyObject指针将会指向t,而s的引用计数减1,如果不存在这样的一个对象,那么就将s存入interned集合中。
还有一点,代码中将s的引用计数减2,这是为什么呢,因为,在将s添加到interned集合中的时候,对于s的引用又多了两次,interned中的指针不能作为s的有效引用,所以就需要在处理过后减去2。在Py_SETREF(*p, t);中,python会将对象p的引用减一从而销毁这个对象,它只是作为一个中间变量,创建之后有可能立即销毁。
字符串缓冲池

PyObject *PyUnicode_DecodeUTF8Stateful(const char *s,
                             Py_ssize_t size,
                             const char *errors,
                             Py_ssize_t *consumed)
{
	...
	if (size == 1 && (unsigned char)s[0] < 128) {
        if (consumed)
            *consumed = 1;
        return get_latin1_char((unsigned char)s[0]);
    }
    ...
}

这里可以看到,当创建的字符串为一个字符时,python会直接从get_latin1_char中获取,而不会重新创建。

get_latin1_char(unsigned char ch)
{
    PyObject *unicode = unicode_latin1[ch];
    if (!unicode) {
        unicode = PyUnicode_New(1, ch);
        if (!unicode)
            return NULL;
        PyUnicode_1BYTE_DATA(unicode)[0] = ch;
        assert(_PyUnicode_CheckConsistency(unicode, 1));
        unicode_latin1[ch] = unicode;
    }
    Py_INCREF(unicode);
    return unicode;
}

python会先创建一个字符对象,并对这个对象i纪念性intern操作,再放到字符串缓冲池中。
PyUnicodeObject的执行效率
在早些python版本中,python字符串对象的“+”操作一直被认为执行效率很低,由于PyUnicodeObject是一个不可变对象,所以python在执行这个操作的时候会重新申请一块内存来将字符串纪念性拼接,如果你只拼接两个字符串可能看不出来什么问题,只要你将很多个字符串同时拼接,这个毛病就会显露出来了,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);

    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;
    }
    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 */
    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;
}

这就是使用“+”拼接字符串时执行的代码。在拼接很多个字符串的时候推荐使用join,因为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;
    }

    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;
    ...
    sz = 0;  // 字符串的总长度
    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;

    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;
}

(这部分代码有点长,只贴了部分代码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值