Python源码剖析[7] —— 字符串对象(2)

来源:互联网 发布:明治奶粉淘宝 编辑:程序博客网 时间:2024/05/19 01:30

[绝对原创 转载请注明出处]

Python源码剖析

——字符串对象PyStringObject(2)

本文作者: Robert Chen(search.pythoner@gmail.com )

3.      Intern机制

无论是PyString_FromString还是PyString_FromStringAndSize,我们都注意到,当字符数组的长度为01时,需要进行了一个特别的动作:PyString_InternInPlace。这就是前面所提到的Intern机制。

PyStringObject对象的Intern机制其目的是对于被Intern之后的字符串,在整个Python运行时,系统中都只有唯一的与该字符串对应的PyStringObject对象。这样当判断两个PyStringObject对象是否相同时,如果它们都被Intern了,那么只需要简单地检查它们对应的PyObject*是否相同即可。这个机制既节省了空间,又简化了对PyStringObject对象的比较,嗯,可谓是一箭双雕哇 :)

假如在某个时刻,我们创建了一个PyStringObject对象A,其表示的字符串是“Python”,在之后的某一时刻,加入我们想为“Python”再次建立一个PyStringObject对象,通常情况下,Python会为我们重新申请内存,创建一个新的PyStringObject对象BAB是完全不同的两个对象,尽管其内部维护的字符数组是完全相同的。

这就带来了一个问题,加入我们在程序中创建了100个“Python”的PyStringObject对象呢?显而易见,这样会大量地浪费珍贵的内存。因此PythonPyStringObject对象引入了Intern机制。在上面的例子中,如果对于A应用了Intern机制,那么之后要创建B的时候,Python会首先在系统中记录的已经被InternPyStringObject对象中查找,如果发现该字符数组对应的PyStringObject对象已经存在了,那么就将该对象的引用返回,而不会创建对象BPyString_InternInPlace正是完成对一个对象的Intern操作:

[stringobjec.c]

void PyString_InternInPlace(PyObject **p)

{

    register PyStringObject *s = (PyStringObject *)(*p);

    PyObject *t;

    if (s == NULL || !PyString_Check(s))

        Py_FatalError("PyString_InternInPlace: strings only please!");

    /* If it's a string subclass, we don't really know what putting

       it in the interned dict might do. */

    if (!PyString_CheckExact(s))

        return;

    if (PyString_CHECK_INTERNED(s))

        return;

 

    if (interned == NULL) {

        interned = PyDict_New();

        if (interned == NULL) {

            PyErr_Clear(); /* Don't leave an exception */

            return;

        }

    }

    t = PyDict_GetItem(interned, (PyObject *)s);

    if (t) {

        Py_INCREF(t);

        Py_DECREF(*p);

        *p = t;

        return;

    }

 

    if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {

        PyErr_Clear();

        return;

    }

    /* The two references in interned are not counted by refcnt.

       The string deallocator will take care of this */

    s->ob_refcnt -= 2;

    PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;

}

 

 

首先会进行一系列的检查。首先,会检查传入的对象是否是一个PyStringObject对象,Intern机制只能应用在PyStringObject对象上,甚至对于它的派生类对象系统都不会应用Intern机制。然后,会检查传入的PyStringObject对象是否已经被Intern机制处理过了,Python不会对同一个PyStringObject对象进行一次以上的Intern操作。

从代码中我们可以清楚地看到,Intern机制的核心在于interned这个东西,那么这个东西是个什么东西呢?

static PyObject *interned;

stringobject.c中的定义我们完全不知道interned是个什么东西,然而在这里我们看到,interned实际上指向的是PyDict_New创建的一个对象。而PyDict_New实际上创建了一个PyDictObject对象,这个对象我们将在后面详细地剖析。其实,现在,一个PyDictObject对象完全可以看作是C++中的map,即map<PyObject*, PyObject*>

现在一切都清楚了,所谓的Intern机制,实际上就是系统中有一个(Key, Value)的映射的集合interned。在这个集合中,记录着被应用了Intern机制的PyStringObject对象。当对一个PyStringObject对象A应用Intern机制时,首先会在Interned中检查是否有满足一下条件的对象BB中维护的原生字符串与A相同。如果确实存在对象B,那么指向APyObject指针将会指向B,而A的引用计数减1,这样,其实A只是一个临时被创建的对象。如果interned中还不存在这样的B,那么就将A记录到interned中。

2展示了如果Interned中存在这样的对象B,在对A进行Intern操作时, 原本指向APyObject指针的变化:

于被InternPyStringObject对象,Python采用了特殊的引用计数机制。在将一个PyStringObject对象APyObject指针作为KeyValue添加到interned中时,PyDictObject对象会通过这两个指针对A的引用计数进行两次加1操作。但是Python的设计者规定在internedA的指针不能被视为对象A的有效引用,因为如果是有效引用的话,那么A的引用计数在Python运行时结束之前永远都不可能为0,因为至少有interned中的两个指针引用了A,那么删除A就永远不可能,这显然是没有道理的。因此interned中的指针不能作为A的有效引用。这也就是在PyString_InternInPlace最后会将引用计数减2的原因。当A的引用计数在某个时刻减为0之后,系统将会销毁对象A,那么我们可以预期,在销毁A的同时,会在interned中删除指向A的指针,显然,这一点在string_dealloc得到了验证:

[stringobject.c]

static void string_dealloc(PyObject *op)

{

    switch (PyString_CHECK_INTERNED(op)) {

        case SSTATE_NOT_INTERNED:

            break;

 

        case SSTATE_INTERNED_MORTAL:

            /* revive dead object temporarily for DelItem */

            op->ob_refcnt = 3;

            if (PyDict_DelItem(interned, op) != 0)

                Py_FatalError(

                    "deletion of interned string failed");

            break;

 

        case SSTATE_INTERNED_IMMORTAL:

            Py_FatalError("Immortal interned string died.");

 

        default:

            Py_FatalError("Inconsistent interned string state.");

    }

    op->ob_type->tp_free(op);

}

 

 

前面提到,Python在创建一个字符串时,会首先在interned中检查是否已经有该字符串对应得PyStringObject对象了,如果有,则不用创建新的,这样可以节省内存空间。事到如今,我必须要承认,我说谎了:)节省内存空间是没错的,可是Python并不是在创建PyStringObject时就通过interned实现了节省空间的目的。事实上,从PyString_FromString中可以看到,无论如何,一个合法的PyString_FromString对象是会被创建的,同样,我们可以注意到,PyString_InternInPlace也只对PyStringObject起作用。事实正是如此,Python始终会为字符串S创建PyStringObject对象,尽管Sinterned中已经有一个与之对应的PyStringObject对象了。而Intern机制是在S被创建后才起作用的,通常Python在运行时创建了一个PyStringObject对象Temp后,基本上都会调用PyString_InternInPlaceIntern机制会减少Temp的引用计数,Temp对象会由于引用计数减为0 而被销毁,它只是作为一个临时对象昙花一现地在内存中闪现,然后湮灭。

那么我们现在有一个疑问了,是否可以直接在C的原生字符串上做Intern的动作,而不需要再创建这样一个临时对象呢?事实上,Python确实提供了一个以char*为参数的Intern机制相关函数,但是你会相当失望,嗯,因为它基本上是换汤不换药的:

[stringobject.c]

PyObject* PyString_InternFromString(const char *cp)

{

    PyObject *s = PyString_FromString(cp);

    if (s == NULL)

        return NULL;

    PyString_InternInPlace(&s);

    return s;

}

 

 

临时对象照样被创建出来,实际上,仔细一想,就会发现在Python中,必须创建这样一个临时的PyStringObject对象来完成Intern操作。为什么呢?答案就在PyDictObject对象interned中,因为PyDictObject必须以PyObject指针作为键。

关于PyStringObject对象的Intern机制,还有一点需要注意。实际上,被InternPyStringObject对象分为两类,一类是SSTATE_INTERNED_IMMORTAL状态的,而另一类是SSTATE_INTERNED_MORTAL状态的,这两种状态的区别在string_dealloc中可以清晰地看到,显然,SSTATE_INTERNED_IMMORTAL状态的PyStringObject对象是永远不会被销毁的,它将与Python run time同年同月同日死。

PyString_InternInPlace只能创建SSTATE_INTERNED_MORTAL状态的PyStringObject对象,如果想创建SSTATE_INTERNED_IMMORTAL状态的对象,必须要通过另外地接口,在调用了PyString_InternInPlace后,强制改变PyStringObjectintern状态。

[stringobject.c]

void PyString_InternImmortal(PyObject **p)

{

    PyString_InternInPlace(p);

    if (PyString_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {

        PyString_CHECK_INTERNED(*p) = SSTATE_INTERNED_IMMORTAL;

        Py_INCREF(*p);

    }

}

4.      字符缓冲池

最后需要注意的一点是与PyIntObject中的小整数对象的对象池一样,Python的设计者为PyStringObject中的一个字节的字符对象也设计了这样一个对象池characters

static PyStringObject *characters[UCHAR_MAX + 1];

其中的UCHAR_MAX是在系统头文件中定义的常量,这也是一个平台相关的常量,在Win32平台下:

#define UCHAR_MAX     0xff      /* maximum unsigned char value */

Python的整数对象体系中,小整数的缓冲池是在Python runtime初始化时被创建的,而字符串对象体系中的字符缓冲池则是以静态变量的形式存在着的。在Python runtime初始化完成之后,缓冲池中的所有PyStringObject指针都为空。

在创建一个PyStringObject时,无论是调用PyString_FromString还是PyString_FromStringAndSize,在创建的字符串实际上是一个字符时,会进行如下的操作:

[stringobject.c]

PyObject* PyString_FromStringAndSize(const char *str, int size)

{

。。。。。。

else if (size == 1 && str != NULL) 

{

        PyObject *t = (PyObject *)op;

        PyString_InternInPlace(&t);

        op = (PyStringObject *)t;

        characters[*str & UCHAR_MAX] = op;

        Py_INCREF(op);

    }

    return (PyObject *) op;

}

 

 

先对所创建的字符串(字符)对象进行Intern操作,再将Intern的结果缓存到字符缓冲池characters中。图3演示了缓存一个字符对象的过程。

3条带有标号的曲线既代表指针,又代表进行操作的顺序:

1)        创建PyStringObject对象”P”

2)        对对象”P”进行Intern操作

3)        将对象”P”缓存至字符缓冲池中

同样,在创建PyStringObject时,会首先检查所要创建的是否是一个字符对象,然后检查字符缓冲池中是否已经有了这个字符的字符对象的缓冲,如果有,则直接返回这个缓冲的对象即可:

[stringobject.c]

PyObject* PyString_FromStringAndSize(const char *str, int size)

{

register PyStringObject *op;

……

    if (size == 1 && str != NULL &&

        (op = characters[*str & UCHAR_MAX]) != NULL)

    {

#ifdef COUNT_ALLOCS

        one_strings++;

#endif

        Py_INCREF(op);

        return (PyObject *)op;

}

……

}


原创粉丝点击