2005年12月26日

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

Python源码剖析

——PyListObject对象(3)

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

3.      PyListObject对象缓冲池

还记得吗,刚才我们按下了一个有趣的话题。没错,就是那个缓冲池,free_list。现在,是揭开它的神秘面纱的时候呢。我们想知道的问题是:free_list中所缓冲的PyListObject对象是从哪里获得的,是在何时创建的。答案就在一个PyListObject被销毁的过程中:

[listobject.c]

static void list_dealloc(PyListObject *op)


{


    int i;


    PyObject_GC_UnTrack(op);


    Py_TRASHCAN_SAFE_BEGIN(op)


    if (op->ob_item != NULL) {


        /* Do it backwards, for Christian Tismer.


           There's a simple test case where somehow this reduces


           thrashing when a *very* large list is created and


           immediately deleted. */


        i = op->ob_size;


        while (--i >= 0) {


            Py_XDECREF(op->ob_item[i]);


        }


        PyMem_FREE(op->ob_item);


    }


    if (num_free_lists < MAXFREELISTS && PyList_CheckExact(op))


        free_lists[num_free_lists++] = op;


    else


        op->ob_type->tp_free((PyObject *)op);


    Py_TRASHCAN_SAFE_END(op)


}

在销毁一个PyListObject的时候,当然要做的一件事是为list中的每一个元素改变其引用计数。然后,我们就来到了最有趣的部分。Python会检查我们开始提到的那个缓冲池,free_lists,查看其中缓存的PyListObject的数量是否已经满了,如果没有,就将该待删除的PyListObject放到缓冲池中,以备后用。现在一切真相大白了,那个在Python启动是空荡荡的缓冲池原来都是被本应该死去的PyListObject对象给填充了 ,在以后创建新的PyListObject的时候,Python会首先唤醒这些已经“死去”的PyListObject。感谢党,感谢政府,又给它们一个重新做“人”的机会:)但是,需要指出,这里缓冲的仅仅是PyListObject对象,而没有这个对象曾经拥有的PyObject*列表,因为这些PyObject指针的引用计数已经减少了,这些指针所指的对象都要各奔前程,或生存,或毁灭,不再被PyListObject所给与的那个引用计数所束缚。PyListObject如果继续维护一个指向这些对象的指针的列表,就可能产生悬空指针的问题。所以,PyObject*列表所占用的空间必须归还给系统。

看一下我们刚刚创建的PyListObject的最后归宿:

在下一次创建PyListObject时,这个PyListObject将重新被唤醒,重新分配PyObject*列表占用的内存,重新拥抱新的对象。放眼四周,曾经所拥有过的那些对象,有的已经容颜苍老,有的已经烟消云散,是否有一种“无私人非事事休,欲语泪先流”的感慨呢?:)

4.      Hack PyListObject

首先我们来观察在PyListObject中维护的元素数量变化时,PyListObjectob_sizeallocated两个变量的变化情况,从中窥见PyListObject对内存的使用和管理。

PyListObject的输出操作list_print中,我们添加了如下代码,以观察PyListObject对内存的管理:

printf("\nallocated=%d, ob_size=%d\n", op->allocated, op->ob_size);

观察结果如图9所示:

    首先创建一个包含一个元素的list,这时ob_sizeallocated都是1。这时list中拥有的所有内存空间都已被使用完,所以下次插入元素时就一定会调整list的内存空间了。

随后向list末尾追加元素2,可以看到,调整内存空间的动作发生了。allocated变成了5,而ob_size则变成了2,这里明确地显示出了PyListObject所采用的与C++vector一样的内存缓冲池策略。

继续向list末尾追加元素345,当追加了元素5之后,list所拥有的内存空间又被使用完了,下一次再追加或插入元素时,内存空间调整的动作又会再一次发生。

如果这时在list中删除元素3,可以看到,ob_size发生了变化,而allocated则不发生变化,它始终如一地维护着当前list所拥有的全部内存数量。

接下来我们从图10的结果中观察一下PyListObject对象的创建和删除对于Python维护的PyListObject对象缓冲池的影响。

次为了消除Python交互环境执行时对PyListObject对象缓冲池的影响,我们通过执行py脚本文件来观察,可以看到,当创建新的PyListObject对象时,如果缓冲池中有可用的PyListObject对象,则会使用缓冲池中的对象;而在销毁一个PyListObject对象时,确实将这个对象放到了缓冲池中。

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

Python源码剖析

——PyListObject对象(2)

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

2.      PyListObject的创建与维护

2.1     创建

Python中只提供了唯一一种创建PyListObject对象的方法—PyList_New

[listobject.c]

PyObject* PyList_New(int size)


{


    PyListObject *op;


    size_t nbytes;




 

    nbytes = size * sizeof(PyObject *);


    /* Check for overflow */


    if (nbytes / sizeof(PyObject *) != (size_t)size)


        return PyErr_NoMemory();




 

    //PyListObject申请空间


    if (num_free_lists) {


        //使用缓冲池


        num_free_lists--;


        op = free_lists[num_free_lists];


        _Py_NewReference((PyObject *)op);


} else {


        //缓冲池中没有可用的对象,创建对象


        op = PyObject_GC_New(PyListObject, &PyList_Type);


}


//PyListObject对象中维护的元素列表申请空间


    if (size <= 0)


        op->ob_item = NULL;


    else {


        op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);


        memset(op->ob_item, 0, nbytes);


    }


    op->ob_size = size;


    op->allocated = size;


    _PyObject_GC_TRACK(op);


    return (PyObject *) op;


}

 

非常清晰的创建过程,首先进行一些例行检查;然后为PyListObject申请内存空间,创建PyListObject对象。再成功创建PyListObject对象之后,就需要为这个对象申请存储PyObject*的内存空间,内存空间的大小由传入的参数确定,传入的参数决定了新创建的PyListObject可以容纳多少个PyObject*。最后将PyListObject对象的存储区域清空,并将ob_sizeallocated都设置为size,为内存管理做好准备。

我们看到,在list的实现中,同样用到了缓冲池的技术,创建PyListObject的时候会首先检查free_lists中是否还有没有使用的PyListObject,如果有就直接使用,只有在free_lists中的PyListObject全部用完之后才会通过malloc在堆上创建新的PyListObjecfree_lists的默认大小为80,对于一般的小程序而言,基本上只会使用到PyListObject缓冲池。

/* Empty list reuse scheme to save calls to malloc and free */

#define MAXFREELISTS 80

static PyListObject *free_lists[MAXFREELISTS];

static int num_free_lists = 0;

 

有一点非常奇特的是,在free_lists中缓存的只是PyListObject*,那么这个缓冲池里的PyListObject究竟是在什么地方被创建的呢?

列位看官,花开两朵,各表一只。我们先把这个问题放一放,看一看,在Python开始运行后,当第一个PyListObject对象被创建时的情形。嗯,这有点像上帝创世纪,挺有趣的,不是吗?:)

在第一个PyListObject创建的时候,我们看到这时num_free_lists0,所以会调用PyObject_GC_New在堆上创建一个新的PyListObject对象,假设我们创建的PyListObject是包含6个元素的PyListObject

一个什么东西都没有的List当然是很无趣的,我们来尝试向里边添加一点东西,把一个整数对象100放到第3个位置上去:

[listobject.c]

int PyList_SetItem(register PyObject *op, register int i, register PyObject   *newitem)


{


    register PyObject *olditem;


    register PyObject **p;


    if (!PyList_Check(op)) {


        ……


    }


    if (i < 0 || i >= ((PyListObject *)op) -> ob_size) {


        Py_XDECREF(newitem);


        PyErr_SetString(PyExc_IndexError,


                "list assignment index out of range");


        return -1;


    }


    p = ((PyListObject *)op) -> ob_item + i;


    olditem = *p;


    *p = newitem;


    Py_XDECREF(olditem);


    return 0;


}

 

先是进行类型检查,然后进行索引的有效性检查,当一切都OK后,将待加入的PyObject指针放到指定的位置,然后将这个位置原来存放的对象的引用计数减1。这里的olditem很可能会是NULL,比如向一个新创建的PyListObject对象加入元素,就会碰到这样的情况,所以这里必须使用Py_XDECREF

好了,现在我们的PyListObject对象再不是当年那个一穷二白的可怜虫了:

2.2     添加

接下来我们再试着向这个PyListObject中插入一个元素,好吧,就在100之前插入9999确实是在100之前的,这个地球人都知道:)

[listobject.c]

int PyList_Insert(PyObject *op, int where, PyObject *newitem)


{


    ......//类型检查


    return ins1((PyListObject *)op, where, newitem);


}


 

static int ins1(PyListObject *self, int where, PyObject *v)


{


    int i, n = self->ob_size;


    PyObject **items;


    ......


    if (list_resize(self, n+1) == -1)


        return -1;




 

    if (where < 0) {


        where += n;


        if (where < 0)


            where = 0;


    }


    if (where > n)


        where = n;


    items = self->ob_item;


    for (i = n; --i >= where; )


        items[i+1] = items[i];


    Py_INCREF(v);


    items[where] = v;


    return 0;


}

 

首先当然是检查指针的有效性,然后是检查当前PyListObject中已经有多少个元素了,如果这个元素个数已经达到了INT_MAX,那么很遗憾,再不能插入任何元素了。

在通过了检查之后,我们看到,调用了一个list_resize函数,从函数名我们可以想象,这个函数改变了PyListObject所维护的PyObject*列表的大小:

[listobject.c]


static int list_resize(PyListObject *self, int newsize)


{


    PyObject **items;


    size_t new_allocated;


    int allocated = self->allocated;




 

    /* Bypass realloc() when a previous overallocation is large enough


       to accommodate the newsize.  If the newsize falls lower than half


       the allocated size, then proceed with the realloc() to shrink the list.


    */


    if (allocated >= newsize && newsize >= (allocated >> 1)) {


        assert(self->ob_item != NULL || newsize == 0);


        self->ob_size = newsize;


        return 0;


    }




 

    /* This over-allocates proportional to the list size, making room


     * for additional growth.  The over-allocation is mild, but is


     * enough to give linear-time amortized behavior over a long


     * sequence of appends() in the presence of a poorly-performing


     * system realloc().


     * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...


     */


    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize;


    if (newsize == 0)


        new_allocated = 0;


    items = self->ob_item;


    if (new_allocated <= ((~(size_t)0) / sizeof(PyObject *)))


        PyMem_RESIZE(items, PyObject *, new_allocated);


    else


        items = NULL;


    if (items == NULL) {


        PyErr_NoMemory();


        return -1;


    }


    self->ob_item = items;


    self->ob_size = newsize;


    self->allocated = new_allocated;


    return 0;


}

插入的时候,Python分四种情况处理:

1.      newsize > ob_size && newsize < allocated :简单调整ob_size值。

2.      newsize < ob_size && newsize > allocated/2 :简单调整ob_size值。

3.      newsize < ob_size && newsize < allocated/2 :调用realloc,重新分配空间。

4.      newsize > ob_size && newsize > allocated :调用realloc,重新分配空间。

可以看出,Python对内存可谓是殚精竭虑了,在某种newsize < ob_size的情况下还会重新申请内存空间。

 

PyListObject的空间调整之后,接着就应该搬动元素了,才好挪出一个位置,插入我们想要插入的元素。在Python中,list支持一个很有趣的特性,就是负值索引,比如一个n个元素的listlst[n],那么lst[-1]就是lst[n-1]。这一点在插入元素时得到了体现。

static int ins1(PyListObject *self, int where, PyObject *v)


{


    ......


    if (where < 0) {


        where += n;


        if (where < 0)


            where = 0;


    }


    if (where > n)


        where = n;


    items = self->ob_item;


    for (i = n; --i >= where; )


        items[i+1] = items[i];


    Py_INCREF(v);


    items[where] = v;


    return 0;


}

 

可以看到,不管你插入在什么位置,对于Python来说,都是合法的,它会自己调整插入的位置。在确定了插入的位置之后,会将插入点之后的所有元素向下挪动一个位置,这样,在插入点就能空出一个位置来,于是大功告成,我们想插入的元素有了容身之地了。

熟悉C++的朋友一定看出来了,这种处理插入的方法实际上与C++中的vector完全一致。实际上,Python中的PyListObjectC++中的vector非常相似,相反,它和C++中的list却是大相径庭的。

值得注意的是,通过与vector类似的内存管理机制,现在,PyListObjectallocated已经变成10了,而ob_size却只有7

Python中,list有一个被广泛使用的操作append。这个操作与上面所描述的插入操作非常类似:

[listobject.c]


int PyList_Append(PyObject *op, PyObject *newitem)


{


    if (PyList_Check(op) && (newitem != NULL))


        return app1((PyListObject *)op, newitem);


    PyErr_BadInternalCall();


    return -1;


}


 

static PyObject* listappend(PyListObject *self, PyObject *v)


{


    if (app1(self, v) == 0)


        Py_RETURN_NONE;


    return NULL;


}


 

static int app1(PyListObject *self, PyObject *v)


{


    int n = PyList_GET_SIZE(self);


......


    if (list_resize(self, n+1) == -1)


        return -1;




 

    Py_INCREF(v);


    PyList_SET_ITEM(self, n, v);


    return 0;


}

只是需要注意的是,在进行append动作的时候,添加的元素是添加在第ob_size个位置上的,而不是第allocated个位置上。下图展示了append元素101之后的PyListObject对象:

app1中调用list_resize时,由于newsize8)在710之间,所以不需要再分配内存空间。直接将101放置在第8个位置上即可。

2.3     删除

除了创建,插入这些操作,一个容器至少还应该有删除操作,PyListObject自然也不会例外。图5展示了一个使用PyListObject中删除元素功能的例子:

当在Python交互环境中输入l.remove(3)时,PyListObject中的listremove操作会被激活:

[listobject.c]

static PyObject * listremove(PyListObject *self, PyObject *v)


{


    int i;


    for (i = 0; i < self->ob_size; i++) {


        int cmp = PyObject_RichCompareBool(self->ob_item[i], v, Py_EQ);


        if (cmp > 0) {


            if (list_ass_slice(self, i, i+1,(PyObject *)NULL) == 0)


                Py_RETURN_NONE;


            return NULL;


        }


        else if (cmp < 0)


            return NULL;


    }


    PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");


    return NULL;


}


 

在遍历PyListObject中所有元素的过程中,将待删除的元素与PyListObject中的每个元素一一进行比较,比较操作是通过PyObject_RichCompareBool完成的。如果发现了匹配,则调用list_ass_slice进行删除操作:

[listobject.c]


static int list_ass_slice(PyListObject *a, int ilow, int ihigh, PyObject *v)


{


   PyObject *recycle_on_stack[8];


    PyObject **recycle = recycle_on_stack; /* will allocate more if needed */

    PyObject **item;


    int n; /* # of elements in replacement list */


    int norig; /* # of elements in list getting replaced */


    int d; /* Change in size */


#define b ((PyListObject *)v)


    if (v == NULL)


        n = 0;


else {


……


}




 

    norig = ihigh - ilow;


    d = n - norig;


item = a->ob_item;


    //[1]

    s = norig * sizeof(PyObject *);

    if (s > sizeof(recycle_on_stack)) {

        recycle = (PyObject **)PyMem_MALLOC(s);

        if (recycle == NULL) {

            PyErr_NoMemory();

            goto Error;

        }

    }

    memcpy(recycle, &item[ilow], s);



 

//[2]

    if (d < 0) { /* Delete -d items */


        memmove(&item[ihigh+d], &item[ihigh],


(a->ob_size - ihigh)*sizeof(PyObject *));


        list_resize(a, a->ob_size + d);


        item = a->ob_item;


    }


……


//[3]

for (k = norig - 1; k >= 0; k)

        Py_XDECREF(recycle[k]);


#undef b


}


 

当传入的vNULL时,就会进行删除的动作,可以看到,这正是listremove期望的动作。首先会获得需要删除的元素个数,这是通过ihigh-ilow得到的,在删除元素这种情况下,这个值显然是1。在获得了需要删除的元素个数之后,在代码的[2]处,list_ass_slice通过memmove来达到删除元素的目的。

很明显了,在PyListObject中,如果是在对象维护的列表中部删除元素的话,一定会引起内存的搬移动作,这一点跟C++中的vector是完全一致的,而与C++中的list则完全不同。

6展示了删除元素100的过程:

值得注意的一点是,实际上,list_ass_slice不仅仅是用做删除元素,它还可以进行插入元素的动作。它的完整功能如下:

a[ilow:ihigh] = v if v != NULL.

del a[ilow:ihigh] if v == NULL.

7展示了一个list_ass_slice进行插入元素操作的例子,有兴趣的朋友可以对list_ass_slice进行深入研究。

2005年12月25日

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

Python源码剖析

——PyListObject对象(1)

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

1.      PyListObject对象

元素的一个群是一个非常重要的抽象概念,我们可以将符合某一特性的一堆元素聚集为一个群,当然,还要可以向群中添加或删除元素。这样的群的概念对于编程语言十分重要,C语言就内建了数组的概念,随着编程语言的发展,现在所有的编程语言都会在语言中或标准库中实现这样的群的概念。而且群的概念还进一步地细分为多种实现方式,比如mapvector等。每一种实现都为某种目的的元素聚集或元素访问提供了极大的方便。

PyListObjectPython提供的对列表的抽象,熟悉C++的人可能会条件反射地将PyListObjectC++中的list对应起来(至少它们名字是相同的:)。而实际上,Python中的列表和C++STL中的vector更为相似。

PyListObject对象可以有效地支持插入,添加,删除等操作,在Python的列表中,无一例外地存放的都是PyObject的指针。所以实际上,你可以这样看待Python中的列表:vector<PyObject*>

很显然,PyListObject一定是一个变长对象,因为不同的list中存储的元素的个数会是不同的。但是,和PyStringObject不同的是,PyListObject对象还支持插入删除等操作,可以在运行时动态地调整其所维护的内存,所以,它还是一个可变对象。

下面看一看PyListObject的声明:

[listobject.h]

typedef struct {


    PyObject_VAR_HEAD


    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */


    PyObject **ob_item;


    int allocated;


} PyListObject;


 

如我们所想,在PyListObject的头,赫然是一个PyObject_VAR_HEAD,随后是一个PyObject**,这个指针正是维护了PyObject*列表的关键。我们知道在PyObject_VAR_HEAD中,有一个ob_size,而在PyListObject的最后,又有一个allocated,那么这两个变量之间的关系是什么呢?

其实,ob_sizeallocated都和内存的管理有关,PyListObject所采用的内存管理策略和C++vector采取的内存分配策略是一样的。它并不是存了多少东西就申请对应大小的内存,这样的申请策略显然是低效的,因为我们有理由相信,用户选用列表正是为了频繁地插入删除元素。因此,PyListObject在每一次需要申请内存的时候,会申请一大块内存,这是申请的总内存的大小记录在allocated中,而这些内存中实际被有效的PyObject*占用的内存大小被记录在了ob_size中。那么不难得到,对于一个PyListObject对象,一定存在以下的关系:

0 <= ob_size <= allocated

len(list) == ob_size

ob_item == NULL implies ob_size == allocated == 0

这里ob_sizeallocated的关系就像C++vectorsizecapacity的关系一样。

2005年12月22日

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

Python源码剖析

——字符串对象PyStringObject(3)

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

5.      PyStringObject效率相关问题

关于PyStringObject,有一个地球人都知道的严重影响Python程序执行效率的问题,有一种说法,绝大部分执行效率特别低下的Python程序都是由于没有注意到这个问题所致。下面我们就来看看这个在Python中举足轻重的问题——字符串连接。

假如现在有两个字符串“Python”和“Ruby”,在JavaC#中,都可以通过使用“+”操作符,将两个字符串连接在一起,得到一个新的字符串“PythonRuby”。当然,Python中同样提供了利用“+”操作符连接字符串的功能,然而不幸的是,这样的做法正是万恶之源。

Python中通过“+”进行字符串连接的方法效率及其低下,其根源在于Python中的PyStringObject对象是一个不可变对象。这就意味着当进行字符串连接时,实际上是必须要创建一个新的PyStringObject对象。这样,如果要连接NPyStringObject对象,那么就必须进行N-1次的内存申请及内存搬运的工作。这将严重影响Python的执行效率。

官方推荐的做法是通过利用PyStringObject对象的join操作来对存储在listtuple中的一组PyStringObject对象进行连接操作,这种做法只需要分配一次内存,执行效率将大大提高。

下面我们通过考察源码来更细致地了解这一问题。

通过“+”操作符对字符串进行连接时,会调用string_concat函数:

[stringobject.c]


static PyObject* string_concat(register PyStringObject *a, register PyObject *bb)


{


    register unsigned int size;


    register PyStringObject *op;


#define b ((PyStringObject *)bb)


    ……


    size = a->ob_size + b->ob_size;


    /* Inline PyObject_NewVar */


    op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);


    if (op == NULL)


        return PyErr_NoMemory();


    PyObject_INIT_VAR(op, &PyString_Type, size);


    op->ob_shash = -1;


    op->ob_sstate = SSTATE_NOT_INTERNED;


    memcpy(op->ob_sval, a->ob_sval, (int) a->ob_size);


    memcpy(op->ob_sval + a->ob_size, b->ob_sval, (int) b->ob_size);


    op->ob_sval[size] = '\0';


    return (PyObject *) op;


#undef b


}


   

对于任意两个PyStringObject对象的连接,就会进行一次内存申请的动作。而如果利用PyStringObject对象的join操作,则会进行如下的动作(假设是对list中的PyStringObject对象进行连接):

[stringobject.c]


static PyObject* string_join(PyStringObject *self, PyObject *orig)


{


    char *sep = PyString_AS_STRING(self);


    const int seplen = PyString_GET_SIZE(self);


    PyObject *res = NULL;


    char *p;


    int seqlen = 0;


    size_t sz = 0;


    int i;


    PyObject *seq, *item;


。。。。。。//获得listPyStringObject对象的个数,保存在seqlen




 

    for (i = 0; i < seqlen; i++)


{


        const size_t old_sz = sz;


        item = PySequence_Fast_GET_ITEM(seq, i);


        sz += PyString_GET_SIZE(item);


        if (i != 0)


            sz += seplen;


    }


/* 申请内存空间 */


    res = PyString_FromStringAndSize((char*)NULL, (int)sz);


    /* 连接list中的每一个PyStringObject对象*/


    p = PyString_AS_STRING(res);


for (i = 0; i < seqlen; ++i)


{


        size_t n;


        /* 获得list中的一个PyStringObject对象*/


        item = PySequence_Fast_GET_ITEM(seq, i);


        n = PyString_GET_SIZE(item);


        memcpy(p, PyString_AS_STRING(item), n);


        p += n;


        if (i < seqlen - 1)


        {


            memcpy(p, sep, seplen);


            p += seplen;


        }


    }


    Py_DECREF(seq);


    return res;


}

   

执行join操作时,会首先统计出在list中一共有多少个PyStringObject对象,并统计这些PyStringObject对象所维护的字符串一共有多长,然后申请内存,将list中所有的PyStringObject对象维护的字符串都拷贝到新开辟的内存空间中。注意,这里只进行了一次内存空间的申请,就完成了NPyStringObject对象的连接操作。相比于“+”操作符,待连接的PyStringObject对象越多,效率的提升也越明显。

6.      Hack PyStringObject

在这一节,我们对PyStringObject在运行时行为进行两项观察。

首先,观察Intern机制,在Python Interactive环境中,创建一个PyStringObject后,就会对这个PyStringObject对象进行Intern操作,所以我们预期内容相同的PyStringObject对象在Intern后应该是同一个对象,图4是观察的结果:

通过在string_length中添加打印地址的代码,我们可以在运行时获得每一个PyStringObject对象的地址,从观察结果中可以看到,无论是对于一般的字符串,还是对于单个字符,Intern机制最终都使不同的PyStringObject指针指向了相同的对象。

然后,我们观察Python中进行缓冲处理的字符对象,同样是通过在string_length中添加代码,打印出缓冲池中从ae的字符对象的引用计数信息。需要注意的是,为了避免执行len(…)对引用计数的影响,我们并不会对a到e的字符对象调用len操作,而是对另外的PyStringObject对象调用len操作:

static void ShowCharater()

{

   char a = ‘a’;

   PyStringObject** posA = characters+(unsigned short)a;

   int i;

   for(i = 0; i < 5; ++i)

   {

      PyStringObject* strObj = posA[i];

      printf("%s, %d\n", strObj->ob_sval, strObj->ob_refcnt);

   }

}

   

5展示了观察的结果,可以看到,在创建字符对象时,Python确实只是使用了缓冲池里的对象,而没有创建新的对象。

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

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;


}


……


}


2005年12月21日

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

字符串对象,在任何一门主流编程语言中,都是整数对象之外使用最广泛的对象。它和整数对象犹如少林,武当,双峰对峙。本章将研究Python中的字符串对象的实现,同整数对象一样,字符串对象的实现中采用了很多额外的机制来保证性能的优化。

 
本章内容分为三个部分:
1. 研究Python中的字符串对象PyStringObject
2. 研究字符串对象的效率加速机制
3. 分析字符串连接操作的效率,Hack PyStringObject

Python源码剖析

——字符串对象PyStringObject(1)

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

1.      PyStringObjectPyString_Type

在对PyIntObject的分析中,我们看到了Python中的具有不可变长度数据的对象(定长对象)。在Python中,还大量存在着另一种对象,即具有可变长度数据的对象(变长对象)。与定长对象不同,对于变长对象而言,对象维护的数据的长度在对象定义时是不知道的。对于PyIntObject来说,其维护的数据的长度在对象定义时就已经确定了,是一个long变量的长度;而可变对象维护的数据的长度只能在对象创建时才能确定,考虑一下,我们只能在创建一个字符串或一个列表时才知道它们所维护的数据的长度,在此之前,对这个信息,我们一无所知。在变长对象中,实际上还可分为可变对象(mutable)和不可变对象(immutable),可变对象是在对象创建之后,其维护的数据的长度还能变化的对象,比如一个list被创建后,可以向其中添加元素或删除元素,这些操作都会改变其维护的数据的长度;而不可变对象所维护的数据在对象创建之后就不能再改变了,比如Python中的stringtuple,它们都不支持添加或删除的操作。本章我们将研究Python变长对象中的不可变对象——字符串对象。

 

Python中,PyStringObject是对字符串对象的抽象和表示。PyStringObject是一个拥有可变长度内存的对象,这一点非常容易理解,因为对于表示”Hi””Python”的两个不同的PyStringObject对象,其内部需要的保存字符串内容的内存空间显然是不一样的。但同时,PyStringObject对象又是一个不变对象(Immutable)。当创建了一个PyStringObject对象之后,该对象内部维护的字符串就不能再被改变了。这一点特性使得PyStringObject对象能作为PyDictObject的键值,但同时也使得一些字符串操作的效率大大降低,比如多个字符串的连接操作。

 

PyStringObject对象的声明如下:

[stringobject.h]

typedef struct {


    PyObject_VAR_HEAD


    long ob_shash;


    int ob_sstate;


    char ob_sval[1];


} PyStringObject;


 

PyStringObject的定义中我们看到,在PyStringObject的头部实际上是一个PyObject_VAR_HEAD,其中有一个ob_size变量保存着对象中维护的可变长度内存的长度。虽然在PyStringObject的定义中,ob_sval是一个字符的字符数组。但是ob_sval实际上是作为一个字符指针指向了一段内存,这段内存保存着这个字符串对象所维护的实际字符串,显然,这段内存不会只是一个字节。而这段内存的实际长度(字节),正是由ob_size来维护,这个机制是Python中所有拥有可变长度内存的对象的实现机制。比如对于PyStringObject对象”Python”ob_size的值就是6

 

C中的字符串一样,PyStringObject内部维护的字符串在末尾必须以’\0’结尾,但是由于字符串的实际长度是由ob_size维护的,所以PyStringObject表示的字符串对象中间是可能出现字符’\0’的,这一点与C语言中不同,因为在C中,只要遇到了字符’\0’,就认为一个字符串结束了。所以,实际上,ob_sval指向的是一段长度为ob_size+1个字节的内存,而且必须满足ob_sval[ob_size] = ‘\0’

 

PyStringObject中的ob_shash变量其作用是缓存该对象的HASH值,这样可以避免每一次都重新计算该字符串对象的HASH值。如果一个PyStringObject对象还没有被计算过HASH值,那么ob_shash的初始值是-1。在计算一个对象的HASH值时,采用如下的算法:

[stringobject.c]


static long string_hash(PyStringObject *a)


{


    register int len;


    register unsigned char *p;


    register long x;




 

    if (a->ob_shash != -1)


        return a->ob_shash;


    len = a->ob_size;


    p = (unsigned char *) a->ob_sval;


    x = *p << 7;


    while (--len >= 0)


        x = (1000003*x) ^ *p++;


    x ^= a->ob_size;


    if (x == -1)


        x = -2;


    a->ob_shash = x;


    return x;


}


 

PyStringObject对象的ob_sstate变量该对象是否被Intern的标志,关于PyStringObjectIntern机制,在后面会详细介绍。

 

下面看一下PyStringObject对应的类型对象:

[stringobject.c]

PyTypeObject PyString_Type = {


    PyObject_HEAD_INIT(&PyType_Type)


    0,


    "str",


    sizeof(PyStringObject),


    sizeof(char),


    ……


    (reprfunc)string_repr,          /* tp_repr */


    &string_as_number,          /* tp_as_number */


    &string_as_sequence,            /* tp_as_sequence */


    &string_as_mapping,         /* tp_as_mapping */


    (hashfunc)string_hash,          /* tp_hash */


    0,                  /* tp_call */


    ……


    string_new,             /* tp_new */


    PyObject_Del,                       /* tp_free */


};

 

可以看到,在PyStringObject的类型对象中,tp_itemsize被设置为sizeof(char),即一个字节。对于Python中的任何一种变长对象,tp_itemsize这个域是必须设置的,tp_itemsize指明了由变长对象保存的元素的单位长度,所谓单位长度即是指一个对象在内存中的长度。这个tp_itemsizeob_size共同决定了应该额外申请的内存的总大小是多少。

 

需要注意的是,我们看到,tp_as_numbertp_as_sequencetp_as_mapping,三个域都被设置了。这表示PyStringObject对数值操作,序列操作和映射操作都支持。

2.      创建PyStringObject对象

Python提供两条路径,从C中原生的字符串创建PyStringObject对象。我们先考察一下最一般的PyString_FromString

[stringobject.c]

PyObject *


PyString_FromString(const char *str)


{


    register size_t size;


    register PyStringObject *op;




 

assert(str != NULL);


/*判断字符串长度*/


    size = strlen(str);


    if (size > INT_MAX) {


        PyErr_SetString(PyExc_OverflowError,


            "string is too long for a Python string");


        return NULL;


}




 

/*处理null string*/


    if (size == 0 && (op = nullstring) != NULL) {


#ifdef COUNT_ALLOCS


        null_strings++;


#endif


        Py_INCREF(op);


        return (PyObject *)op;


    }


    if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {


#ifdef COUNT_ALLOCS


        one_strings++;


#endif


        Py_INCREF(op);


        return (PyObject *)op;


    }




 

    /* 创建新的PyStringObject对象,并初始化 */


    /* Inline PyObject_NewVar */


    op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);


    if (op == NULL)


        return PyErr_NoMemory();


    PyObject_INIT_VAR(op, &PyString_Type, size);


    op->ob_shash = -1;


    op->ob_sstate = SSTATE_NOT_INTERNED;


memcpy(op->ob_sval, str, size+1);




 

    /* Itern(共享)长度较短的PyStringObject对象 */


    if (size == 0) {


        PyObject *t = (PyObject *)op;


        PyString_InternInPlace(&t);


        op = (PyStringObject *)t;


        nullstring = op;


        Py_INCREF(op);


    } else if (size == 1) {


        PyObject *t = (PyObject *)op;


        PyString_InternInPlace(&t);


        op = (PyStringObject *)t;


        characters[*str & UCHAR_MAX] = op;


        Py_INCREF(op);


    }


    return (PyObject *) op;


}

 

显然,传给PyString_FromString的参数必须是一个指向以NULL结尾的字符串的指针。在从一个原生字符串创建PyStringObject时,首先需要检查该字符数组的长度,如果字符数组的长度大于了MAX_INT,那么Python将不会创建对应得PyStringObject对象。MAX_INT在系统的头文件中定义,是一个平台相关的值,在WIN32系统下,该值为:

#define INT_MAX 2147483647

嗯,这个界限值确实非常庞大了,如果不是由于变态,几乎没有人会去试图超越这个禁区的 :)

 

接下来,将会检查传入的字符串是不是一个空串,对于空串,Python并不是每一次都会创建相应得PyStringObjectPython运行时有一个PyStringObject对象指针nullstring专门负责处理空的字符数组。如果第一次在一个空字符串基础上创建PyStringObject,由于nullstring指针被初始化为NULL,所以Python会为这个空字符建立一个PyStringObject对象,将这个PyStringObject对象通过Intern机制进行共享,然后将nullstring指向这个被共享的对象。如果在以后Python检查到需要为一个空字符串创建PyStringObject对象,这时nullstring已经存在了,那么就直接返回nullstring的引用。

 

接下来需要进行的动作就是申请内存,创建PyStringObject对象。可以看到,这里申请的内存除了PyStringObject的内存,还有为字符数组内的元素申请的额外的内存。然后,将HASH缓存值设为-1,将Intern标志设为SSTATE_NOT_INTERNED。最后将字符数组内的字符拷贝到PyStringObject所维护的空间中,在拷贝的过程中,将字符数组最后的’\0’字符也拷贝了。加入我们对于字符数组”Python”建立PyStringObject对象,那么对象建立完成后在内存中的状态如图1所示:

 

PyString_FromString之外,还有一条创建PyStringObject对象的途径:PyString_FromStringAndSize

[stringobject.c]


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


{


    register PyStringObject *op;


/*处理null string*/


if (size == 0 && (op = nullstring) != NULL) {


#ifdef COUNT_ALLOCS


        null_strings++;


#endif


        Py_INCREF(op);


        return (PyObject *)op;


    }


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


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


    {


#ifdef COUNT_ALLOCS


        one_strings++;


#endif


        Py_INCREF(op);


        return (PyObject *)op;


    }




 

    /* Inline PyObject_NewVar */


    op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);


    if (op == NULL)


        return PyErr_NoMemory();


    PyObject_INIT_VAR(op, &PyString_Type, size);


    op->ob_shash = -1;


    op->ob_sstate = SSTATE_NOT_INTERNED;


    if (str != NULL)


        memcpy(op->ob_sval, str, size);


    op->ob_sval[size] = '\0';


    /* share short strings */


    if (size == 0) {


        PyObject *t = (PyObject *)op;


        PyString_InternInPlace(&t);


        op = (PyStringObject *)t;


        nullstring = op;


        Py_INCREF(op);


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


}


PyString_FromStringAndSize的操作过程和PyString_FromString一般无二,只是有一点,PyString_FromString传入的参数必须是以NULL结尾的字符数组的指针,而PyString_FromStringAndSize不会有这样的要求,因为通过传入的size参数就可以确定需要拷贝的字符的个数

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

Python源码剖析

——整数对象PyIntObject(3)

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

3         Hack PyIntObject

现在,让我们荡起双桨,哦不对,让我们挽起衣袖和裤脚J,来和PyIntObject大战一场。我们渴望在运行时观察Python的整数对象体系的变化。这一点,完全可以通过修改Python源码来实现。我们修改了int_print的行为,让它打印出关于block_listfree_list的信息,以及小整数缓冲池的信息:

static int int_print(PyIntObject *v, FILE *fp, int flags)

{

   PyIntObject* intObjectPtr;

   PyIntBlock *p = block_list;

   PyIntBlock *last = NULL;

   int count = 0;

   int i;

 

   while(p != NULL)

   {

      ++count;

      last = p;

      p = p->next;

   }

 

   intObjectPtr = last->objects;

   intObjectPtr += N_INTOBJECTS – 1;

   printf("address @%p\n", v);

   printf("********** value\trefCount **********\n");

   for(i = 0; i < 10; ++i, –intObjectPtr)

   {

      printf("%d\t\t%d\n", intObjectPtr->ob_ival, intObjectPtr->ob_refcnt);

   }

 

   printf("block_list count : %d\n", count);

   printf("free_list : %p\n\n", free_list);

 

   return 0;

}

需要特别注意的是,在初始化小整数缓冲池时,对于block_list以及每个PyIntBlockobjects,都是从后往前开始填充的,所以在初始化完成后,-5应该在最后一个PyIntBlock对象的objects内最后一块内存,所以我们需要顺藤摸瓜,一直找到这最后的一块内存,才能观察从-5410个小整数。

首先我们创建一个PyIntObject对象-9999,从图10所示的输出信息可以看到,小整数对象很多都被Python自身使用多次了。

现在的free_list指向地址为00B6AF9C的内存,根据上面对PyIntObject的分析,那么下一个PyIntObject会在这个地址安身立命。那么好,我们接着再建立了两个PyIntObject对象,它们的值分别是-123456

从图11所示的结果中可以看到a的地址正是创建ifree_list所指的地址,而b的地址也正是创建afree_list所指的地址。虽然ab的值都是一样的,但是它们确实是两个完全没有关系的PyIntObject对象,这点从地址上看得一清二楚。

现在我们将b删除,结果如图12所示:

删除b后,free_list回退到了a创建后free_list的位置,这一点也跟前面的分析是一致的。

最后我们来看一看对小整数对象的监控,连续两次创建PyIntObject对象-5,结果如图13所示:

可以看到,两次创建的PyIntObject对象d1d2的地址都是一样的,这证明它们实际上是同一个对象。同时,我们看到小整数池中-5的引用计数发生了变化,这证明d1d2实际上都是指向这个对象。此外,free_list没有发生任何变化。这些都与我们对PyIntObject的分析相符

2005年12月19日

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

Python源码剖析

——整数对象PyIntObject(2)

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

2         PyIntObject对象的创建和维护

2.1       对象创建的三种途径

intobject.h中可以看到,可以从三种途径获得一个PyIntObject对象:

  PyObject *PyInt_FromLong(long ival)


  PyObject* PyInt_FromString(char *s, char **pend, int base)


#ifdef Py_USING_UNICODE


PyObject*PyInt_FromUnicode(Py_UNICODE *s, int length, int base)


#endif


 

分别是从long值,从字符串以及Py_UNICODE对象生成PyIntObject对象。在这里我们只考察从long值以及字符串生成PyIntObject对象。因为PyInt_FromString实际上是先将字符串转换成浮点数,然后再调用PyInt_FromFloat

[intobject.c]


PyObject* PyInt_FromString(char *s, char **pend, int base)


{


    char *end;


    long x;


    ......


//convert string to long


if (base == 0 && s[0] == '0')


{


        x = (long) PyOS_strtoul(s, &end, base);


    }


else


        x = PyOS_strtol(s, &end, base);


    ......


    return PyInt_FromLong(x);


}


 

为了理解整数对象的创建过程,必须要深入了解Python中整数对象在内存中的组织方式,实际上,一个个的整数对象在内存中并不是独立存在,单兵作战的,而是形成了一个整数对象体系。我们首先就重点考察一下Python中的整数对象体系结构。

2.2       小整数对象

在实际的编程中,对于数值比较小的整数,比如1229等等,可能在程序中会非常频繁地使用。想一想C语言中的for循环,就可以了解这些小整数会有多么频繁的使用场合。在Python中,所有的对象都是存活在系统堆上。这就是说,如果没有特殊机制,对于这些频繁使用的小整数对象,Python将一次又一次地在使用malloc在堆上申请空间,并不厌其烦地一次次free。这样的操作不仅大大降低了运行效率(Python本来就以速度慢被人诟病了 :),而且会在系统堆上造成内存碎片,严重影响Python的整体性能。

显然,Guido是决不能容许这样的方案存在的,于是在Python中,对小整数对象,使用了对象池技术。刚才我们说了,PyIntObject对象是Immutable对象,这带来了一个天大的喜讯,所以对象池里的PyIntObject对象能够被任意地共享。

给你一个整数100,你说它是个“小”整数吗?那么101呢?小整数和大整数的分界线在哪里?Python的回答是“it’s all up on you”,你想它在哪里它就在哪里。

Python中提供了一种方法,通过这种方法,用户可以动态地调整小整数与大整数的分界线,从而动态确定对象池中到底应该有多少个对象。呃,但是,老实说,Python提供的这种方法比较原始,为了达到动态调整的目的,你只有自己修改源代码。

[intobject.c]

#ifndef NSMALLPOSINTS


#define NSMALLPOSINTS       100


#endif


#ifndef NSMALLNEGINTS


#define NSMALLNEGINTS       5


#endif


#if NSMALLNEGINTS + NSMALLPOSINTS > 0


/* References to small integers are saved in this array so that they


   can be shared.


   The integers that are saved are those in the range


   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).


*/


static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];


#endif


 

Python2.4中,将小整数集合的范围默认设定为[-5100)。但是你完全可以修改NSMALLPOSINTSNSMALLNEGINTS,重新编译Python,从而将这个范围向两端伸展或收缩。

对于小整数对象,Python直接将这些整数对应的PyIntObject缓存在内存中。那么对于大整数呢?很显然,整数对象是编程中使用得非常多的东西,谁敢保证只有小整数才会被频繁地使用呢。如果将所有的整数对应的PyIntObject对象都缓存在内存池中,自然是再理想不过了,但是这样对内存的使用是会被视为败家子,难免遭人鄙视的:)。时间与空间的两难选择,这个计算机领域最基本的矛盾在这里浮出水面。

2.3       大整数对象

Python的设计者们所做出的妥协是,对于小整数,完全地缓存其PyIntObject对象。而对其它整数,Python运行环境将提供一块内存空间,这些内存空间由这些大整数轮流使用,也就是说,谁需要的时候谁就使用。这样免去了不断地malloc之苦,又在一定程度上考虑了效率问题。我们下面将详细剖析其实现机制。

Python中,有一个PyIntBlock结构,在这个结构的基础上,实现了的一个单向列表。

[intobject.c]

#define BLOCK_SIZE  1000    /* 1K less typical malloc overhead */


#define BHEAD_SIZE  8   /* Enough for a 64-bit pointer */


#define N_INTOBJECTS    ((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject))




 

struct _intblock {


    struct _intblock *next;


    PyIntObject objects[N_INTOBJECTS];


};




 

typedef struct _intblock PyIntBlock;




 

static PyIntBlock *block_list = NULL;


static PyIntObject *free_list = NULL;


 

PyIntBlock,顾名思义,就是说这个结构里维护了一块(block)内存,其中保存了一些PyIntObject对象。从PyIntBlock的声明中可以看到,在一个PyIntBlock中维护着N_INTOBJECTS对象,做一个简单的计算,就可以知道是82个。显然,这个地方也是Python的设计者留给你的可以动态调整地地方,不过,你需要再一次地修改源代码并重新编译。

PyIntBlock的单向列表通过block_list维护,而这些block中的PyIntObject的列表中可以被使用的内存通过free_list来维护。最开始的时候,这两个指针都被设置为空指针,如图3所示。

(注意:此后,我们将用红色箭头表示free_list,蓝色箭头表示block_list

2.4       添加和删除

好了,现在我们大体上了解了Python中整数对象在内存中的体系结构。下面通过对PyInt_FromLong的考察,真实地展现一个个PyIntObject对象是如何从无到有地产生并融入到Python的整数对象体系中的。

[intobject.c]


PyObject* PyInt_FromLong(long ival)


{


    register PyIntObject *v;


#if NSMALLNEGINTS + NSMALLPOSINTS > 0


    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {


        v = small_ints[ival + NSMALLNEGINTS];


        Py_INCREF(v);


#ifdef COUNT_ALLOCS


        if (ival >= 0)


            quick_int_allocs++;


        else


            quick_neg_int_allocs++;


#endif


        return (PyObject *) v;


    }


#endif


    if (free_list == NULL) {


        if ((free_list = fill_free_list()) == NULL)


            return NULL;


    }


    /* Inline PyObject_New */


    v = free_list;


    free_list = (PyIntObject *)v->ob_type;


    PyObject_INIT(v, &PyInt_Type);


    v->ob_ival = ival;


    return (PyObject *) v;


}


 

在调用PyInt_FromLong时,首先会检查传入的long值是否属于小整数的范围,如果确实是属于小整数,那么很简单了,只需要返回在对象池中的对应的对象就可以了。

如果传入的long值不是属于小整数,Python就会转向由block_list维护的内存。当首次调用PyInt_FromLong时,因为free_listNULL,这时会调用fill_free_list

[intobject.c]


static PyIntObject* fill_free_list(void)


{


    PyIntObject *p, *q;


    /* Python's object allocator isn't appropriate for large blocks. */


    p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));


    if (p == NULL)


        return (PyIntObject *) PyErr_NoMemory();


    ((PyIntBlock *)p)->next = block_list;


    block_list = (PyIntBlock *)p;


    /* Link the int objects together, from rear to front, then return


       the address of the last int object in the block. */


    p = &((PyIntBlock *)p)->objects[0];


    q = p + N_INTOBJECTS;


    while (--q > p)


        q->ob_type = (struct _typeobject *)(q-1);


    q->ob_type = NULL;


    return p + N_INTOBJECTS - 1;


}


 

fill_free_list中,会申请一个PyIntBlock结构体的对象,如图4所示:

(注意:图中的虚线并不表示指针关系,虚线表示objects的更详细地表示方式,下同)

 

然后,会将该Block中的PyIntObject数组中的所有的PyIntObject对象通过指针依次连接起来,就像一个单向链表一样。在这里,使用了PyObject中的ob_type指针作为连接指针。可以看到,Python的设计者为了解决问题,也就不再考虑什么类型安全了。就像政治一样,计算机也是一门妥协的艺术 :)。fill_free_list完成后最终的Block如图5所示。可以看到,这时候,free_list也在它该出现的位置了。从free_list开始,沿着ob_type指针,就可以遍历刚刚创建的PyIntBlock对象中的所有可使用的为PyIntObject准备的内存了,如图5所示:

当一个Block中还有剩余的内存没有被一个PyIntObject占用时,free_list就不会指向NULL。所以在这种情况下调用PyInt_FromLong不会申请新的Block。只有在一个Block中的内存都被占用了,PyInt_FromLong才会再次调用fill_free_list申请新的空间,为新的PyIntObject创造新的家园。图6展示了两次申请BlockBlock链表的情况。值得注意的是,block_list始终是指向最新创建的PyIntBlock对象。

PyInt_FromLong中,当必要的空间被申请之后,将会把当前可用的Block中的内存空间划出一块,将在这块内存上创建我们需要的PyIntObject对象,同时,还会调整完成必要的初始化工作,以及调整free_list指针,使其指向下一块还没有被占用的内存。

从图中我们发现,两个PyIntBlock处于同一个链表当中,但是每一个PyIntBlock中至关重要的存放PyIntObject对象的objects却是分离的。这样的结构存在着隐患,考虑这样的情况:

现在有两个PyIntBlock对象,PyIntBlock1PyIntBlock2PyIntBlock1中的objects已经被PyIntObject对象填满,而PyIntBlock2种的object只填充了一部分。所以现在free_list指针指向的是PyIntBlock2.objects中空闲得内存块。假设现在PyIntBlock1.objects中的一个PyIntObject对象被删除了,现在PyIntBlock1中有空闲的内存可用了,那么下次创建新的PyIntObject对象时应该使用PyIntBlock1中的这块内存。那么如何使Python意识到这块重获自由的内存呢?如果象上图所示的PyIntBlock对象间的objects没有任何联系,那么显然不可能实现这样的功能,所以它们之间一定存在联系。实际上,不同PyIntBlock对象之间的空闲内存块是被链接在一起的,形成了一个单向链表,表头就是free_list

那么,不同PyIntBlock中的空闲内存块是在什么时候被链接在一起的呢,这一切都发生在一个PyIntObject对象被销毁的时候。

列位看官,花开两朵,各表一支:)这里我们先放下自由内存链表,仔细考察一下一个PyIntObject对象在被销毁时都发生了什么事。在对Python中对象机制的分析中,我们已经看到,每一个对象都有一个引用计数与之相连,当这个引用计数减少到0时,就意味着这个世上再也没有谁需要它了,于是Python运行时会负责将这个对象销毁。Python中不同对象在销毁时会进行不同的动作,销毁动作在与对象对应的类型对象中被定义,这个关键的操作就是类型中的tp_dealloc

下面看一看PyIntObject对象的tp_dealloc操作:

[intobject.c]


static void int_dealloc(PyIntObject *v)


{


    if (PyInt_CheckExact(v)) {


        v->ob_type = (struct _typeobject *)free_list;


        free_list = v;


    }


    else


        v->ob_type->tp_free((PyObject *)v);


}


 

在前面我们说了,由block_list维护的PyIntBlock的列表中的内存实际是所有的大整数对象所共同分享的。俗话说,皇帝轮流坐,明年到我家。当一个PyIntObject对象被删除时,它所占有的内存并没有被释放,归还给系统,而是继续被保留着。但是这一块内存现在已经是归free_list所维护的链表所有了,这表明在PyIntObject对象被删除后,它所占用的内存成了一块自由内存,可以供别的PyIntObject使用了。int_dealloc完成的就是这么一个简单的指针维护的工作。当然,这些动作是在删除的对象确实是一个PyIntObject对象时发生的。如果删除的对象是一个整数的派生类的对象,那么int_dealloc不做任何动作,只是简单地调用派生类型中制定的释放函数。

在图7中我们形象地展示相继创建和删除PyIntObject对象,在这一过程中,内存中的PyIntObject对象以及free_list指针的变化情况。同时我们还展示了small_ints这个小整数的对象池。

 

现在回头来看看刚才提到的不同PyIntBlock对象的objects间的空闲内存的互连问题。其实很简单,不同PyIntBlock对象中空闲内存的互连也是在int_dealloc被调用时实现的。图8展示了这个过程(黄色表示空闲内存)。

2.5       小整数对象池的初始化

现在,关于Python的整数对象体系,我们只剩下最后一个问题了。在small_ints中,我们看到,它维护的只是PyIntObject的指针,那么这些与天地同寿的小整数对象是在什么地方被创建和初始化的呢。完成这一切的神秘的函数正是_PyInt_Init

[intobject.c]


int _PyInt_Init(void)


{


    PyIntObject *v;


    int ival;


#if NSMALLNEGINTS + NSMALLPOSINTS > 0


for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++)


{


        if (!free_list && (free_list = fill_free_list()) == NULL)


            return 0;


        /* PyObject_New is inlined */


        v = free_list;


        free_list = (PyIntObject *)v->ob_type;


        PyObject_INIT(v, &PyInt_Type);


        v->ob_ival = ival;


        small_ints[ival + NSMALLNEGINTS] = v;


    }


#endif


    return 1;


}


这些永生不灭的小整数对象也是生存在由block_list所维护的内存上,在Python运行时初始化的时候,_PyInt_Init被调用,内存被申请,小整数对象被创建,然后就仙福永享,寿与天齐了 :)

 

2005年12月18日

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

作为Python中最简单的对象,整数对象是研究Python对象体系的一个非常好的切入点。直观上会认为整数对象的实现非常简单,如果单纯以整数对象而言,实现确实非常简单。然而在Python中,为了运行效率,实际上存在着一个以缓冲池为核心的整数对象的体系结构,实际上,Python各种对象几乎都拥有这样一个以缓冲池为核心的体系结构,理解这一点对Python运行时行为的了解有重要的意义。对这种结构的深入挖掘也是本章的重点所在。

本章分为三个部分:

1. 研究Python中的整数对象 :PyIntObject

2. 通过研究PyIntObject的创建和维护,深入挖掘整数对象体系结构

3. Hack PyIntObject :通过修改Python源代码,对第一第二部分的知识加深了解

本文是第一部分。

Python源码剖析

——整数对象PyIntObject(1)

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

1         PyIntObject

Python中,整数对象是最简单的对象了。对于读者来说,也是最容易真切地感受Python的对象机制的切入点,因此我们对Python所有内建对象的研究就从这个最简单的整数对象开始。

Python中的整数是通过PyIntObject对象来实现的。PyIntObject的对象是一个不变(immutable)对象,也就是说,在创建了一个PyIntObject的对象之后,我们就再也不能改变该对象的值了。在Python中,除了整数对象之外,还有很多对象也是不变对象,比如字符串对象等。

首先,我们先来关注一下PyIntObject的定义:

[intobject.h]


typedef struct {


    PyObject_HEAD


    long ob_ival;


} PyIntObject;


 

可以看到,Python中的PyIntObject实际上就是对Clong的一个简单包装。从对Python的对象机制的描述中,我们知道,对于Python中的对象,与对象相关的有用的元信息实际上都是保存在与对象对应的类型对象中的,对于PyIntObject,它的类型对象就是PyInt_Type

[intobject.c]


PyTypeObject PyInt_Type = {


    PyObject_HEAD_INIT(&PyType_Type)


    0,


    "int",


    sizeof(PyIntObject),


    0,


    (destructor)int_dealloc,        /* tp_dealloc */


    (printfunc)int_print,           /* tp_print */


    0,                  /* tp_getattr */


    0,                  /* tp_setattr */


    (cmpfunc)int_compare,           /* tp_compare */


    (reprfunc)int_repr,         /* tp_repr */


    &int_as_number,             /* tp_as_number */


    0,                  /* tp_as_sequence */


    0,                  /* tp_as_mapping */


    (hashfunc)int_hash,         /* tp_hash */


    0,                  /* tp_call */


    (reprfunc)int_repr,         /* tp_str */


    PyObject_GenericGetAttr,        /* tp_getattro */


    0,                  /* tp_setattro */


    0,                  /* tp_as_buffer */


    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES | Py_TPFLAGS_BASETYPE, /* tp_flags */


    int_doc,                /* tp_doc */


    0,                  /* tp_traverse */


    0,                  /* tp_clear */


    0,                  /* tp_richcompare */


    0,                  /* tp_weaklistoffset */


    0,                  /* tp_iter */


    0,                  /* tp_iternext */


    int_methods,        /* tp_methods */


    0,                  /* tp_members */


    0,                  /* tp_getset */


    0,                  /* tp_base */


    0,                  /* tp_dict */


    0,                  /* tp_descr_get */


    0,                  /* tp_descr_set */


    0,                  /* tp_dictoffset */


    0,                  /* tp_init */


    0,                  /* tp_alloc */


    int_new,                /* tp_new */


    (freefunc)int_free,                 /* tp_free */


};


 

可以看到,在PyInt_Type中保存着PyIntObject的元信息,其中有PyIntObject对象的大小,PyIntObject的文档信息,更多的是PyIntObject所支持的操作。在下面的列表中,列出了PyIntObject所支持的操作:

int_dealloc

删除PyIntObject对象

int_free

删除PyIntObject对象

int_repr

转化成PyString对象

int_hash

获得HASH

int_print

打印PyIntObject对象

int_compare

比较操作

int_as_number

数值操作

int_methods

成员函数

 

我们可以看一看比较操作的代码:

[intobject.c]


static int int_compare(PyIntObject *v, PyIntObject *w)


{


    register long i = v->ob_ival;


    register long j = w->ob_ival;


    return (i < j) ? -1 : (i > j) ? 1 : 0;


}


 

可以看到,PyIntObject对象实际上就是简单地将它所包装的long值进行比较。

需要特别注意的是int_as_number这个域,实际上它是一个PyNumberMethods结构体:

[intobject.c]


static PyNumberMethods int_as_number = {


    (binaryfunc)int_add,    /*nb_add*/


    (binaryfunc)int_sub,    /*nb_subtract*/


    (binaryfunc)int_mul,    /*nb_multiply*/


    ……


    (binaryfunc)int_div,    /* nb_floor_divide */


    int_true_divide,    /* nb_true_divide */


    0,          /* nb_inplace_floor_divide */


    0,          /* nb_inplace_true_divide */


};


 

object.h中,可以找到PyNumberMethods的定义。在Python2.4中,PyNumberMethods中一共有38个函数指针,也就是说在其中定义了38种操作的信息,这些都是作为一个数值(Number)类型的对象可以向世界提供的操作,比如,加法,减法,乘法,模运算等等。

int_as_number中,确定了对于一个整数对象,这些数值操作应该如何进行。当然,在PyNumberMethods38中数值操作中,并非所有的操作都要求一定要被实现,在int_as_number中我们就可以看到,有相当多的操作是没有实现的。我们可以看一下PyIntObject中加法操作是如何实现的:

[intobject.h]


/* Macro, trading safety for speed */


#define PyInt_AS_LONG(op) (((PyIntObject *)(op))->ob_ival)


 

[intobject.c]


#define CONVERT_TO_LONG(obj, lng)       \


    if (PyInt_Check(obj)) {         \


        lng = PyInt_AS_LONG(obj);   \


    }                   \


    else {                  \


        Py_INCREF(Py_NotImplemented);   \


        return Py_NotImplemented;   \


    }


 

static PyObject *


int_add(PyIntObject *v, PyIntObject *w)


{


    register long a, b, x;


    CONVERT_TO_LONG(v, a);


    CONVERT_TO_LONG(w, b);


    x = a + b;


    if ((x^a) >= 0 || (x^b) >= 0)


        return PyInt_FromLong(x);


    return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);


}


 

如你所想,PyIntObject实现的加法操作是直接在其维护的long值上进行的,可以看到,在完成了加法操作后,还进行了溢出的检查,如果没有溢出,就返回一个新的PyIntObject,这个PyIntObject所拥有的值正好是加法操作的结果。这里清晰地显示了,PyIntObject是一个Immutable的对象,因为在操作完成之后,原来参与操作的任何一个对象都没有发生改变,取而代之的,一个全新的对象诞生了。而如果加法的结果有溢出,那么结果就再不是一个PyIntObject对象,而是一个PyLongObject了,如图1所示:

1

PyInt_Type中的int_doc域中维护着PyIntObject的文档信息,你可以在Python的交互环境下通过下列命令看到这段文档,如图2所示:

2

[python.h]


/* Define macros for inline documentation. */


#define PyDoc_VAR(name) static char name[]


#define PyDoc_STRVAR(name,str) PyDoc_VAR(name) = PyDoc_STR(str)


#ifdef WITH_DOC_STRINGS


#define PyDoc_STR(str) str


#else


#define PyDoc_STR(str) ""


#endif




 

[intobject.c]


PyDoc_STRVAR(int_doc,


"int(x[, base]) -> integer\n\


\n\


Convert a string or number to an integer, if possible.  A floating point\n\


argument will be truncated towards zero (this does not include a string\n\


representation of a floating point number!)  When converting a string, use\n\


the optional base.  It is an error to supply a base when converting a\n\


non-string. If the argument is outside the integer range a long object\n\


will be returned instead.");


2005年12月17日

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

Python源码剖析

——对象机制

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

1.      对象

Python的世界中,一切都是对象,一个整数是一个对象,一个字符串也是一个对象,更为奇妙的是,类型也是一个对象,整数类型是一个对象,字符串类型也是一个对象。从1980Guido在那个圣诞节揭开Python世界的大幕开始,一直到现在,Python经历了一次一次的升级,但是其实现语言一直都是ANSI C。我们知道,C并不是一个面向对象的语言,那么在Python中,它的对象机制是如何实现的呢?

对于人的思维来说,对象是一个比较形象的概念,而对于计算机来说,对象实际上是一个抽象的概念。计算机并不能理解这是一个整数,那是一个字符串,对于计算机来说,它所知道的一切都是字节。通常的说法是,对象是数据以及基于这些数据的操作的集合。在计算机上,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也有可能是离散的,这都不重要,重要的是这片内存在更高的层次上可以作为一个整体来考虑,这个整体就是一个对象。在这片内存中,存储着一系列的数据以及可以对这些数据进行修改或读取的一系列操作的代码。

Python中,对象就是在堆上申请的结构体,对象不能是被静态初始化的,并且也不能是在栈空间上生存的。唯一的例外就是类型对象(type object)Python中所有的类型对象都是被静态初始化的。

Python中,一个对象一旦被创建,它在内存中的大小就是不变的了。这就意味着那些需要容纳可变长度数据的对象只能在对象内维护一个指向一个可变大小的内存区域的指针。为什么要设定这样一条特殊的规则呢,因为遵循这样的规则可以使通过指针维护对象的工作变得非常的简单。因为一旦允许对象的大小可在运行期改变,我们可以考虑如下的情形。在内存中有对象A,并且其后紧跟着对象B。如果运行期某个时刻,A的大小增大了,这意味着必须将整个A移动到内存中的其他位置,否则A增大的部分将覆盖原本属于B的数据。一旦将A移动到内存中的其他位置,那么所有指向A的指针必须立即得到更新,光是想一想,就知道这样的工作是多么的恐怖。

Python中,所有的东西都是对象,而所有的对象都拥有一些相同的内容,这些内容在PyObject中定义,PyObject是整个Python对象机制的核心。

[object.h]

typedef struct _object {

    PyObject_HEAD

} PyObject;

 

实际上,PyObjectPython中不包含可变长度数据的对象的基石,而对于包含可变长度数据的对象,它的基石是PyVarObject

[object.h]

typedef struct {


    PyObject_VAR_HEAD


} PyVarObject;


 

这两个结构体构成了Python对象机制的核心基石,从代码中我们可以看到,Python的对象的秘密都隐藏在PyObject_HEADPyObject_VAR_HEAD中。

[object.h]

#ifdef Py_TRACE_REFS


/* Define pointers to support a doubly-linked list of all live heap objects. */


#define _PyObject_HEAD_EXTRA        \


    struct _object *_ob_next;   \


    struct _object *_ob_prev;




 

#define _PyObject_EXTRA_INIT 0, 0,




 

#else


#define _PyObject_HEAD_EXTRA


#define _PyObject_EXTRA_INIT


#endif




 

/* PyObject_HEAD defines the initial segment of every PyObject. */


#define PyObject_HEAD           \


    _PyObject_HEAD_EXTRA        \


    int ob_refcnt;          \


    struct _typeobject *ob_type;




 

#define PyObject_VAR_HEAD       \


    PyObject_HEAD           \


    int ob_size; /* Number of items in variable part */


 

PyObject_HEAD中定义了每一个Python对象都必须有的内容,这些内容将出现在每一个Python对象所占有的内存的最开始的字节中,从PyObject_VAR_HEAD的定义可以看出,即使对于拥有可变大小数据的对象,其最开始的字节也含有相同的内容,这就是说,在Python中,每一个对象都拥有相同的对象头部。这就使得在Python中,对对象的引用变得非常的统一,我们只需要用一个PyObject *就可以引用任意的一个对象,而不论该对象实际是一个什么对象。

PyObject_HEAD的定义中,我们注意到有一个ob_refcnt的整形变量,这个变量的作用是实现引用计数机制。对于某一个对象A,当有一个新的PyObject *引用该对象时,A的引用计数应该增加;而当这个PyObject *被删除时,A的引用计数应该减少。当A的引用计数减少到0时,A就可以从堆上被删除,以释放出内存供别的对象使用。

PyObject_HEAD中,我们注意到ob_type是一个指向_typeobject结构体的指针,那么这个结构体是一个什么东西呢?实际上这个结构体也是一个对象,它是用来指定一个对象类型的类型对象。这个类型对象我们将在后边详细地考察。现在我们看到了,在Python中实际上对象机制的核心非常的简单,一个是引用计数,一个就是类型。

而对于拥有可变长度数据的对象,这样的对象通常都是容器,我们可以在PyObject_VAR_HEAD中看到ob_size这个变量,这个变量实际上就是指明了该对象中一共包含了多少个元素。注意,ob_size指明的是元素的个数,而不是字节的数目。比如对于Python中最常用的list,它就是一个PyVarObject对象,如果某一时刻,这个list中有5个元素,那么PyVarObject.ob_size的值就是5

2.      类型对象

在上面的描述中,我们看到了Python中所有对象的对象头的定义。所以,当内存中存在某一个Python的对象时,该对象的开始的几个字节的含义一定会符合我们的预期。但是,当我们把眼光沿着时间轴上溯,就会发现一个问题。当在内存中分配空间,创建对象的时候,毫无疑问地,必须要知道申请多大的空间。显然,这不会是一个定值,因为对于不同的对象,需要不同的空间,一个整数对象和一个字符串对象所需的空间肯定不同。那么,对象所需的内存空间的大小的信息到底在哪里呢?在对象头中显然没有这样的信息。

实际上,内存空间大小这样的对象的元信息是与对象所属类型密切相关的,因此它一定会出现在与对象所对应的类型对象中。现在我们可以来详细考察一下类型对象_typeobject

[object.h]

typedef struct _typeobject {


    PyObject_VAR_HEAD


    char *tp_name; /* For printing, in format "<module>.<name>" */


    int tp_basicsize, tp_itemsize; /* For allocation */




 

    /* Methods to implement standard operations */


    destructor tp_dealloc;


printfunc tp_print;


……


    /* More standard operations (here for binary compatibility) */


    hashfunc tp_hash;


    ternaryfunc tp_call;


    ……


} PyTypeObject;


 

_typeobject的定义中包含了许多的信息,主要可以分为四类:

1.类型名,tp_name,主要是Python内部以及调试的时候使用;

2.创建该类型对象是分配内存空间的大小的信息,即tp_basicsizetp_itemsize

3.与该类型对象相关联的操作信息,比如hashfunctp_hash就指明对于该类型的对象,如何生成其hash值。在Object.h中可以看到,hashfunc实际上是一个函数指针:typedef long (*hashfunc)(PyObject *); _typeobject中,包含了大量的函数指针,这些函数指针将用来指定某个类型的操作信息。这些操作主要分为标准操作(dealloc, print, compare),标准操作族(numbers, sequences, mappings),以及其他操作(hash, buffer, call…)。

4.我们在下边将要描述的类型的类型信息。

有趣的是我们在_typeobject的头部发现了PyObject_VAR_HEAD,这意味着类型实际上也是一个对象。我们知道在Python中,每一个对象都是对应一种类型的,那么一个有趣的问题就出现了,类型对象的类型是什么呢?这个问题听上去很绕口,实际上确非常重要,对于其他的对象,可以通过与其关联的类型对象确定其类型,那么通过什么来确定一个对象是类型对象呢?答案就是PyType_Type

[typeobject.c]

PyTypeObject PyType_Type = {


    PyObject_HEAD_INIT(&PyType_Type)


    0,                  /* ob_size */


    "type",                 /* tp_name */


    sizeof(PyHeapTypeObject),       /* tp_basicsize */


    sizeof(PyMemberDef),            /* tp_itemsize */


    ……


    PyObject_GC_Del,                /* tp_free */


    (inquiry)type_is_gc,            /* tp_is_gc */


};


 

前面提到,在Python中,每一个对象它的开始部分都是一样的。每一个对象都将自己的引用计数,类型信息保存在开始的部分中。为了方便对这部分内存的初始化,Python中提供了几个有用的宏:

[object.h]


#ifdef Py_TRACE_REFS


#define _PyObject_EXTRA_INIT 0, 0,


#else


#define _PyObject_EXTRA_INIT


#endif




 

#define PyObject_HEAD_INIT(type)    \


    _PyObject_EXTRA_INIT        \


    1, type,


 

再回顾一下PyObjectPyVarObject的定义,初始化的动作就一目了然了。实际上,这些宏在类型对象的初始化中被大量地使用着。

如果以一个整数对象为例,可以更清晰地看到一半的类型对象和这个特立独行的PyType_Type对象之间的关系:

[intobject.c]

PyTypeObject PyInt_Type = {


    PyObject_HEAD_INIT(&PyType_Type)


    0,


    "int",


    sizeof(PyIntObject),


    ……


};


 

现在我们可以放飞想象,看到一个整数对象在运行时的抽象的表示了,下图中的箭头表示ob_type


 

 

3.      对象间的继承和多态

通过PyObject和类型对象,Python利用C语言完成了C++所提供的继承和多态的特性。前面提到,在Python中所有的内建对象(PyIntObject等)和内部使用对象(PyCodeObject等)的最开始的内存区域都拥有一个PyObject。实际上,这一点可以视为PyIntObjectPyCodeObject等对象都是从PyObject继承而来。

Python创建一个对象,比如PyIntObject对象时,会分配内存,进行初始化。然后这个对象会由一个PyObject*变量来维护,而不是通过一个PyIntObject*指针来维护。其它对象也与此类似,所以在Python内部各个函数之间传递的都是一种范型指针PyObject*。这个指针所指的对象究竟是什么类型的,不知道,只能从指针所指对象的ob_type域判断,而正是通过这个域,Python实现了多态机制。

考虑下面的代码:

void Print(PyObject* object)

{

    object->ob_type->tp_print(object);

}

如果传给Print的指针实际是一个PyIntObject*,那么就会调用PyIntObject对象对应的类型对象中定义的输出操作,如果传给Print的指针实际是一个PyStringObject*,那么就会调用PyStringObject对象对应的类型对象中定义的输出操作。可以看到,这里同一个函数在不同情况下表现出了不同的行为,这正是多态的核心所在。

object.c中,Python实现了一些对于类型对象中的各种操作的简单包装,从而为Python运行时提供了一个统一的多态接口层:

[object.c]

long PyObject_Hash(PyObject *v)

{

    PyTypeObject *tp = v->ob_type;

    if (tp->tp_hash != NULL)

        return (*tp->tp_hash)(v);

    ……

}

4.      引用计数

CC++中,程序员被赋予了极大的自由,可以任意地申请内存。但是权利的另一面则对应着责任,程序员必须自己负责将申请的内存释放,并释放无效指针。可以说,这一点正是万恶之源,大量的内存泄露和悬空指针的bug由此而生,如黄河泛滥一发不可收拾 :)

现代的开发语言中一般都选择由语言本身负责内存的管理和维护,即采用了垃圾收集机制,比如JavaC#。垃圾收集机制使开发人员从维护内存分配和清理的繁重工作中解放出来,但同时也剥夺了程序员与内存亲密接触的机会,并付出了一定的运行时效率作为代价。现在看来,随着垃圾收集机制的完善,对时间要求不是非常高的程序完全可以通过使用垃圾收集机制的语言来完成,这部分程序占了这个星球上大多数的程序。这样做的好处是提高了开发效率,并降低了bug发生的机率。Python同样也内建了垃圾收集机制,代替程序员进行繁重的内存管理工作,而引用计数正式Python垃圾收集机制的一部分。

Python通过对一个对象的引用计数的管理来维护对象在内存中的生存。我们知道在Python中每一个东西都是一个对象,都有一个ob_refcnt变量,正是这个变量维护着该对象的引用计数,从而也最终决定着该对象的生生灭灭。

Python中,主要是通过Py_INCREF(op)Py_DECREF(op)两个宏来增加和减少一个对象的引用计数。当一个对象的引用计数减少到0之后,Py_DECREF将调用该对象的析构函数(deallocator function)来释放该对象所占有的内存和系统资源。注意这里的析构函数借用了C++的词汇,实际上这个析构动作是通过在对象对应的类型对象中定义的一个函数指针来刻画的,还记得吗?就是那个tp_dealloc

如果熟悉设计模式中Observer模式,可以看到,这里隐隐约约透着Observer模式的影子。在ob_refcnt减为0之后,将触发对象销毁的事件;从Python的对象体系来看,各个对象又提供了不同的事件处理函数,而事件的注册动作正是在各个对象对应的类型对象中静态完成的。

对于这两个宏的参数op来说,不允许op是一个指向空对象的指针(NIL),如果op是一个NIL,那么必须使用Py_XINCREF/Py_XDECREF这一对宏。

PyObject中我们看到ob_refcnt是一个32位的整形变量,这实际是一个Python所做的假设,即对一个对象的引用不会超过一个整形变量的最大值。一般情况下,如果不是恶意代码,这个假设显然是不会被突破的。

需要注意的是,在Python的各种对象中,类型对象是超越引用计数规则的。类型对象“跳出三界外,不再五行中”,永远不会被析构。每一个对象中指向类型对象的指针不被视为对类型对象的引用。

在每一个对象创建的时候,Python提供了一个_Py_NewReference(op)宏来将对象的引用计数初始化为1

Python的源代码中可以看到,在不同的编译选项下(Py_REF_DEBUG, Py_TRACE_REFS),引用计数的宏还要做许多额外的工作。下面展示的代码是Python在最终发行时这些宏所对应的实际的代码:

[object.h]

/* Without Py_TRACE_REFS, there’s little enough to do that we expand code

 * inline.

 */

#define _Py_NewReference(op) ((op)->ob_refcnt = 1)

 

#define _Py_Dealloc(op) ((*(op)->ob_type->tp_dealloc)((PyObject *)(op)))

 

#define Py_INCREF(op) ((op)->ob_refcnt++)

 

#define Py_DECREF(op)                   \

    if (–(op)->ob_refcnt != 0)         \

        ;            \

    else                        \

        _Py_Dealloc((PyObject *)(op))

 

/* Macros to use in case the object pointer may be NULL: */

#define Py_XINCREF(op) if ((op) == NULL) ; else Py_INCREF(op)

#define Py_XDECREF(op) if ((op) == NULL) ; else Py_DECREF(op)

 

在一个对象的引用计数减为0时,与该对象对应的析构函数就会被调用,但是要特别注意的是,调用析构函数并不意味着最终一定会调用free释放内存空间,如果真是这样的话,那频繁地申请、释放内存空间会使Python的执行效率大打折扣(更何况Python已经多年背负了人们对其执行效率的指责:)。一般来说,Python中大量采用了内存对象池的技术,使用这种技术避免频繁地申请和释放内存空间。因此在析构时,通常都是将对象占用的空间归还到内存池中。这一点在接下来对Python内建对象的实现中可以看得一清二楚。

5.      Python对象的分类

我们将Python的对象从概念上大致分为四类,需要指出的是,这种分类并不一定完全正确,不过是提供一种看待Python中对象的视角而已:

l        Math :数值对象

l        Container :容纳其他对象的集合对象

l        Composition :表示程序结构的对象

l        Internal Python解释器在运行时内部使用的对象

2列出了我们的对象分类体系,并给出了每一个类别中的一些实例:

6.      通向Python之路

Python源码的剖析将分为四部分。

1.静态对象剖析:首先我们会分析静态的对象,Math对象和Container对象,深刻理解这些对象对我们理解Python解释器的运行会有很大的帮助,同时,对我们编写Python代码也将大有裨益,在编写Python代码时,你会清晰地意识到系统内部这些对象将如何运作,变化。当然,我们并不会分析所有的Python对象,而是选取使用最频繁的四种对象:PyIntObject, PyStringObject, PyListObject, PyDictObject进行剖析。

2.运行时剖析:在分析完静态的对象之后,我们将进入Python解释器,在这里我们会详细地考察Python的字节码(byte code)以及解释器对字节码的解释和执行过程。这部分将完整地展现Python中所有的语法结构,如一般表达式,控制流,异常流,函数,类等等的字节码层面的实现细节。同时,在这部分,我们会考察大部分的Python内部对象。

3.编译期剖析:这部分没什么好打广告的了,目标明确,对象清晰,但是难度呢,绝不简单 :)

4.运行环境剖析:这部分将考察从激活PythonPython准备就绪,可以接受用户输入或者执行脚本文件,这段时间内,Python如何建立自己的运行环境,并建立了怎样的运行环境,呵呵透露一下,想想Python那个庞大的builtin函数集合,这些就是这部分考察的重点。

阅读完这些内容之后,对于Python,你应该是了如指掌了,在以后编写Python代码时,你的脑子里甚至可以出现Python解释器将如何一步步解释你的代码的情形。当然,这只是我写作本书的副产品。这本书诞生的真正原因只有一个,兴趣,我对Python的实现有浓厚的兴趣。这本书也只是第一步,希望以后还能继续对Python系列,如IronPythonJythonPyPy的探索,当然,对于其他动态语言,比如Ruby的探索,我希望也会有时间去做。如果你对动态语言的实现有兴趣,你一定会喜欢本书;如果你还没有兴趣,希望它能唤起你的兴趣 :)