[Python源码剖析] Python的字符串对象

Chilly_Rain posted @ 2013年8月01日 21:54 in Python源码剖析 , 2626 阅读

如前面提到的,Python的对象可分为可变对象与不可变对象,从另一个角度又可以分为变长对象和定长对象。前一章中的PyIntObject属于不可变对象+定长对象,而本章中的PyStringObject则是不可变对象+变长对象。也就是说,一个Python字符串的长度是非固定的(不同的字符串长度很可能不一样),而当它创建起来之后,就不可以再修改了(因此也可以作为dict的键)。这种不可变性也会使某些字符串操作的效率降低,比如多个字符串的拼接就不得不生成若干的中间对象。

PyStringObject的定义如下

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

PyObject_VAR_HEAD宏展开后有一个名为ob_size的变量,它记录了这个字符串的元素个数,而这个字符串的起始位置是由ob_sval指针来指向。这两个指变共同定义了这个字符串,由于有ob_size这个变量,所以可以允许ob_sval到ob_sval+ob_size之间存在'\0',但是这段内存也必须满足ob_sval[ob_size] == '\0'。另外的两个变量,ob_shash缓存了该对象hash值,初始值为-1;ob_sstate标记了该对象是否已经过intern机制的处理,这个后面会提到。

PyStringObject对应的类型对象是PyString_Type,它也是PyTypeObject的一个实例。其中的tp_itemsize记录了元素的单位长度(即所占用的内存大小),这个tp_itemsize和字符串对象中的ob_size共同决定了这个字符串所占用的内存大小。

字符串的创建有两个方式,PyString_FromString以及PyString_FromStringAndSize,前者要求传入一个以'\0'结尾的字符串,后者则还要传入一个字符串的长度,因此可以允许字符串中包括'\0'。两者差不多,主要看PyString_FromString。

PyObject* PyString_FromString(const char* str)
{
    register size_t size;
    register PyStringObject* op;
    size = strlen(str);
    if(size > PY_SSIZE_T_MAX) {
        return NULL;
    }
    if(size == 0 && (op = nullstring) != NULL) {
        return (PyObject*) op;
    }
    if(size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
        return (PyObject*) op;
    }
    op = (PyStringObject*) PyObject_MALLOC(sizeof(PyStringObject) + size);
    PyObject_INIT_VAR(op, &PyString_Type, size);
    op->ob_shash = -1;
    op->sstate = SSTATE_NOT_INTERNED;
    memcpy(op->ob_sval, str, size+1);
    ...
    return (PyObject*) op;
}

首先检查字符串长度,太大的话拒绝创建;nullstring是一个特殊的字符串对象,表示空串。第一次创建空字符串对象时,nullstring为NULL,所以会走正常流程创建一个PyStringObject,并通过intern机制共享出来,未来再有要求创建长度为0的字符串时就直接返回它了。最后面的一段是真正的创建字符串对象,为PyStringObject及其内部的字符串数组申请内存并初始化,包括对ob_shash和sstate的初始化。

实际上,在return之前,函数内部还做一个intern的动作:

PyObject *t = (PyObject*)op;
PyString_InternInPlace(&t);
op = (PyStringObject*)t;

字符串在被intern之后,整个程序运行期间就只有唯一一个PyStringObject与这个字符串对应,不节省了空间,又简化了对PyStringObject的比较。下面就是这个intern具体的工作:

void PyString_InternInPlace(PyObject** p)
{
    register PyStringObject *s = (PyStringObject*)(*p);
    PyObject* t;
    if(!PyString_CheckExact(s)) {
        return;
    }
    if(PyString_CHECK_INTERNED(s)) {
        return;
    }
    if(interned == NULL) {
        interned = PyDict_New();
    }
    t = PyDict_GetItem(interned, (PyObject*)s);
    if(t) {
        Py_INCREF(t);
        Py_DECREF(*p);
        *p = t;
        return;
    }
    PyDict_SetItem(interned, (PyObject*)s, (PyObject*)s);
    s->ob_refcnt -= 2
    PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

首先要检查传入的对象必须是一个严格的PyStringObject对象,甚至它的派生类都不会做intern。然后,检查这个对象是否已经被intern过了,如果是的话,直接返回,啥都不会干了。再后面就看到,这个intern机制实际就是维护了一个Python dict,它的key和val都是PyObject*类型的。

通过对引用计数的调整也可以看出,这个dict内对字符串对象的引用是不算在引用计数里面的,否则这个对象一旦被intern了,就永远无法释放了。而当一个字符串对象的引用计数降为0时,在string_dealloc销毁这个对象本身的同时,也是清除掉其在interned结构中的指针。

当有一个相同的字符串创建请求时,无论如何会创建出对应的PyStringObject,因为interned这个dict本身就需要以PyStringObject*作为key的。但是如果在intern检查已存在这个对应的PyStringObject,那么会返回intern中的对象,而刚刚创建的那个临时字符串对象就会被瞬间销毁了。

还有一点,被intern的字符串有两种状态:SSTATE_INTERNED_MORTAL, SSTATE_INTERNED_IMMORTAL,区别在于string_dealloc后者是永远不会被销毁的。PyString_InternInPlace只能创建前者,而后则需要调另外的函数接口PyString_InternImmortal,而它实际就是在调用了PyString_InternInPlace之后强制把状态改成了SSTATE_INTERNED_IMMORTAL。

前面在介绍PyString_FromString创建字符串对象时,漏掉了一个地方,就是当size == 1时的情况,此时会用上一个characters的静态数组

static PyStringObject* charaters[UCHAR_MAX+1];

创建之前会先检查要创建的这个字符串对象是否已经存在于characters这个数组中了,如果是的话就直接返回了,不需要再往下执行了。而这个数组是如何创建的呢?实际也在这个PyString_FromString之中,如下

PyStringObject *t = (PyObject*)op;
PyString_InternInPlace(&t);
op = (PyStringObject*)t;
characters[*str&UCHAR_MAX] = op;
Py_INCREF(op);

也就是说,单个字符的PyStringObject在创建后,像其它字符串一样被intern,接下来又被放入了characters这个数组中。就像PyIntObject的小整数对象一样,既在small_ints数据中,也在block_list管理的结构之中。

最后是关于效率的一点提示,由于PyStringObject是不可变对象,因此当有N个字符串连接时,就必须N-1次的内存申请及内存搬运工作,这必然会影响执行效率。官方推荐的方法是先把这些字符串放到一个list或者tuple中,然后使用join操作,这样就只分配一次内存即可。

 

Avatar_small
luovinooo 说:
2013年8月16日 14:17

你好,我最近也是在阅读和学习《python源码剖析》这本书,同时也创建了一个QQ(330938015)群,但是我不知道怎么样 才能把自己所想的用一个很普通的语言写成一个Blog分享出来,希望你一起来讨论和学习,期待着你的回复。。。。

Avatar_small
www.upboard12thresu 说:
2023年7月01日 18:58

The intermediate yearly final public examination test results for all government and private college general and vocational course IA, ISC, and ICOM students for the academic year will be right now, the Lucknow and Allahabad boards'www.upboard12thresult2019.com officials have not provided any information on the date of the intermediate examination's final exam result announcement for the Arts, Commerce, and Science group. Neither has the BHSIEUP.

Avatar_small
AP Inter 1st Year Bl 说:
2023年7月11日 15:07

Board of Intermediate Education, Andhra Pradesh is a Board of education in Andhra Pradesh, BIEAP Office is now Located in Vijayawada After State Reorganization in 2024. AP Inter Board Offers Two-year AP Inter 1st Year Bleprint 2024 Courses in 85 Streams and Courses and Conducts Examination. BIEAP is Responsible for Organising and Conducting the Inter 1st Year Exam across the state of Andhra Pradesh, AP Inter Board Provides almost 85 courses for Students. AP Inter 1st Year Blueprint Prescribed by the BIEAP board will give an Overview of the Courses that are Covered for the 1st year Students During the Academic year.

Avatar_small
seo service london 说:
2024年2月21日 22:53

I would like to thank you for the efforts you have made in writing this article. I am hoping the same best work from you in the future as well..


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter