A921222d247204727d7f7e4e9fd1778b
Python的字符串对象

题外话

每当我们学习一门新的编程语言的时候,接触到的第一个范例基本都是输出"hello world"。随着学习的深入,我们也逐步了解了各项语言中字符串类型的使用和特性。看起来只是由一个个字符类型组成的集合体(这里我们暂时不考虑各种unicode字符等优化措施),使用起来也相对直接简洁,但是从高级编程语言使用的角度和从C语言层面来设计类型是两个完全不同的角度。因此,我把Python字符串对象的剖析放到了现在,使得大家在阅读前面几章节后,对于对象结构的设计已经有了一定的感觉,能够更好的切入今天的文章。

变长、定长、可变、不可变

如果你阅读过我前面几章节的内容关于Python的整数对象设计以及数组对象的设计,相信你能够对变长和定长对象作出一定的区别。除了长度变化这个区分度以外,对象在被创建后的修改状态也是另一个维度的区分。字符串对象作为一种长度可变的对象,在创建后并不可变。

我们可以通读一下[stringobject.h]暴露的API,可以发现字符串对象并没有暴露类似之前数组对象的append, remove等修改内部数据的操作。

PyStringObject

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];
} PyStringObject;

整体上Python的字符串对象结构非常简洁,还是利用PyObject_VAR_HEAD中的ob_size来表示字符串中包含多少字符。

当然,由于Python的底层是C实现的,因此字符串的也有一个隐式的要求:结尾的字符必须是\0但是由于比其纯C的char *表达多了一层ob_size的数量维护,因此Python中并不会自动按照\0截断字符串,而是实际会使用ob_size + 1范围来确定性的获取这段内存中的字符值。

而真正的字符串内容则是存放在ob_sval中,之所以使用char []的形式而不是char *,则可以有效的利用ob_size以及ob_sval的地址来访问连续内存。

ob_shash则是用于缓存当前字符串的hash值。还记得我们最早提过,任何一个对象都可以实现三个方法簇:

  • as_number
  • as_sequence
  • as_mapping

ob_shash就是一种懒加载方式的as_mapping中需要的hash计算缓存。由于字符串对象是不可变对象,因此无需多次计算自身的hash值。

通过PyStringObject的元类型PyType_string,我们来一起看看这个ob_shash是如何计算的:

static long
string_hash(PyStringObject *a)
{
    register Py_ssize_t len;
    register unsigned char *p;
    register long x;

#ifdef Py_DEBUG
    assert(_Py_HashSecret_Initialized);
#endif

    1. 缓存
    if (a->ob_shash != -1)
        return a->ob_shash;
    len = Py_SIZE(a);
    /*
      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) {
        a->ob_shash = 0;
        return 0;
    }
    p = (unsigned char *) a->ob_sval;
    x = _Py_HashSecret.prefix;
    x ^= *p << 7;
    while (--len >= 0)
        x = (1000003*x) ^ *p++;
    x ^= Py_SIZE(a);
    x ^= _Py_HashSecret.suffix;
    if (x == -1)
        x = -2;
    a->ob_shash = x;
    return x;
}

其中初始化的时候ob_shash-1,代表未计算过。否则就执行下述的逻辑计算hash值。

Python字符串的初始化

PyObject *
PyString_FromString(const char *str)
{
    register size_t size;
    register PyStringObject *op;

    assert(str != NULL);
    size = strlen(str);
    if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) {
        PyErr_SetString(PyExc_OverflowError,
            "string is too long for a Python string");
        return NULL;
top Created with Sketch.