Python源码剖析(04 Python中的List对象)

来源:互联网 发布:ipad淘宝可以看直播吗 编辑:程序博客网 时间:2024/05/19 01:11

PyListObject是Python提供的对列表的抽象,Python中的列表和C++的STL中list是大相径庭,相反,它与STL中的vector却更为神似。

4.1 PyListObject对象

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

PyListObject的定义:

[listobject.h]typedef struct {    PyObject_VAR_HEAD    //ob_item为指向元素列表的指针,实际上,Python中的list[0]就是ob_item[0]    PyObject **ob_item;    int allocated;} PyListObject;

ob_size和allocated都和PyListObject对象的内存管理有关,PyListObject所采用的内存管理策略和C++中vector采取的内存管理策略是一样的。我们有理由相信,用户选用列表正是为了频繁地插入或删除元素。所以,在每一次需要申请内存的时候,PyListObject总会申请一大块内存,这时申请的总内存的大小记录在allocated中,而其中实际被使用了的内存的数量则记录在了ob_size中。假如有一个能容纳10个元素的PyListObject对象已经装入了5个元素,那对于这个PyListObject对象,其ob_size为5,而allocated则为10。
存在一下的关系:

0 <= ob_size <= allocatedlen(list) == ob_sizeob_item == NULL 意味着 ob_size == allocated == 0

这里ob_size和allocated的关系就像C++的vector中size和capacity的关系一样。从这里我们实际上已然可以窥见PyListObject管理元素的策略了。

4.2 PyListObject对象的创建与维护

4.2.1 创建对象

Python只提供了唯一的一条途径——PyList_New。这个函数接受一个size参数,从而允许我们可以在创建一个PyListObject对象的同时指定该列表初始的元素个数。需要注意,这里仅仅指定了元素的个数,并没有指定元素是什么。

[listobject.c]PyObject* PyList_New(int size){    PyListObject *op;    size_t nbytes;    //[1] :内存数量计算,溢出检查    nbytes = size * sizeof(PyObject *);    if (nbytes / sizeof(PyObject *) != (size_t)size)        return PyErr_NoMemory();    //[2] :为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);    }    //[3] :为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;    return (PyObject *) op;}

首先,Python在代码清单4-1的1处会计算需要使用的内存总量,因为PyList_New指定的仅仅是元素的个数,而不是元素实际将占用的内存空间。在这里,Python会检查指定的元素个数是否会大到使所需内存数量产生溢出的程度,如果会产生溢出(如果溢出,nbytes会是负数),那么Python将不会进行任何动作。
Python中的列表对象实际上是分为两部分的,一是PyListObject对象本身,二则是PyListObject对象维护的元素列表。这是两块分离的内存,它们通过ob_item建立了联系。
在代码清单4-1的2创建新的PyListObject对象时,我们看到了现在已经非常熟悉的Python对象级的缓冲池技术。在创建PyListObject对象时,会首先检查缓冲池free_lists中是否有可用的对象,如果有,则直接使用这个可用对象;如果缓冲池中所有对象都不可用,则会通过PyObject_GC_New在系统堆中申请内存,创建新的PyList- Object对象。实际上,PyObject_GC_New除了申请内存之外,还会为Python中的自动垃圾收集机制做一些准备工作,这里,我们只需要将PyObject_ GC_New想象成malloc即可。在Python 2.5中,默认情况下,free_lists中最多会维护80个PyListObject对象。

[listobject.c]    #define MAXFREELISTS 80        static PyListObject *free_lists[MAXFREELISTS];    static int num_free_lists = 0;

完成了PyListObject对象及其维护的列表的创建之后,Python会调整该PyListObject对象,用于维护元素列表中元素数量的ob_size和allocated两个变量。
细心的读者一定注意到了,在代码清单4-1的2处提及的PyListObject对象缓冲池,实际有一个很奇特的地方。我们看到,在free_lists中缓存的只是PyListObject*,那么这个缓冲池里的PyListObject*究竟指向什么地方呢?换句话说,这些PyListObject*指向的PyListObject对象是在何时何地被创建的呢?我们先把这个问题放一放,看一看在Python开始运行时,第一个PyListObject对象被创建时的情形。

4.2.2 设置元素

在第一个PyListObject创建的时候,这时的num_free_lists是0,所以在代码清单4-1的2处会绕过对象缓冲池,转而调用PyObject_GC_New在系统堆上创建一个新的PyListObject对象,假设我们创建的PyListObject是包含6个元素的PyListObject,也就是通过PyList_New(6)来创建PyListObject对象
图4_1
需要注意的是,当我们在Python的交互式环境或者.py源文件中创建一个list时,内存中的PyListObject对象中元素列表中的元素不可能是NULL。这里我们只是为了演示元素列表的变化,所以不必在意元素是否可以为NULL。
一个什么东西都没有的list当然是很无趣的,我们来尝试向里边添加一点东西,把一个整数对象100放到第4个位置上去,用Python的行话来说就是list3 = 100(见代码清单4-2)。

# 代码清单4-2[listobject.c]int PyList_SetItem(register PyObject *op, register int i, register PyObject                         *newitem){    register PyObject *olditem;    register PyObject **p;    //[1] :索引检查    if (i < 0 || i >= ((PyListObject *)op) -> ob_size) {        PyErr_SetString(PyExc_IndexError, "list assignment index out of          range");        return -1;    }    //[2] :设置元素    p = ((PyListObject *)op) -> ob_item + i;    olditem = *p;    *p = newitem;    Py_XDECREF(olditem);    return 0;}

当我们在Python中运行list3 = 100时,在Python内部,就是调用PyList_SetItem来完成这个动作。首先Python会进行类型检查,在这里我们省略了。随后,在代码清单4-2的[1]处,会进行索引的有效性检查。当类型检查和索引有效性检查都顺利通过之后,Python在代码清单4-2的[2]处将待加入的PyObject*指针放到指定的位置,然后调整引用计数,将这个位置原来存放的对象的引用计数减1(将原来位置上的值删除,当然要-1啦~)。这里的olditem很可能会是NULL,比如向一个新创建的PyListObject对象加入元素,就会碰到这样的情况,所以这里必须使用Py_XDECREF。
图4_2

4.2.3 插入元素

设置元素和插入元素的动作是不同的,设置元素不会导致ob_item指向的内存发生变化,而插入元素的动作则有可能使得ob_item指向的内存发生变化。图4-3中显示了设置元素和插入元素的区别:
图4_3
从图4-3的结果可以看到,这个插入动作确实导致了元素列表的内存的变化。接下来会深入地剖析插入元素的动作是如何导致元素列表的内存发生变化的(见代码清单4-3)

# 代码清单4-3[listobject.c]int PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem){    ......//类型检查    return ins1((PyListObject *)op, where, newitem);}static int ins1(PyListObject *self, Py_ssize_t where, PyObject *v){    Py_ssize_t i, n = self->ob_size;    PyObject **items;    ......    //[1] :调整列表容量    if (list_resize(self, n+1) == -1)        return -1;    //[2] :确定插入点    if (where < 0) {        where += n;        if (where < 0)            where = 0;    }    if (where > n)        where = n;    //[3] :插入元素    items = self->ob_item;    for (i = n; --i >= where; )        items[i+1] = items[i];    Py_INCREF(v);    items[where] = v;    return 0;}

在ins1中,为了完成元素的插入工作,必须首先保证一个条件得到满足,那就是PyListObject对象必须有足够的内存来容纳我们期望插入的元素。Python通过在代码清单4-3的1处调用了list_resize函数来保证该条件一定能成立。

 [listobject.c]static int list_resize(PyListObject *self, int newsize){    PyObject **items;    size_t new_allocated;    int allocated = self->allocated;    //不需要重新申请内存    if (allocated >= newsize && newsize >= (allocated >> 1)) {        self->ob_size = newsize;        return 0;    }    //计算重新申请的内存大小    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize;    if (newsize == 0)        new_allocated = 0;    //扩展列表    items = self->ob_item;    PyMem_RESIZE(items, PyObject *, new_allocated); //最终调用C中的realloc    self->ob_item = items;    self->ob_size = newsize;    self->allocated = new_allocated;    return 0;}

在调整PyListObject对象所维护的列表的内存时,Python分两种情况处理:

  • newsize < allocated && newsize > allocated/2 :简单调整ob_size值;
  • 其他情况,调用realloc,重新分配空间。

可以看出,Python对内存可谓是殚精竭虑了,甚至在第2种情况中,当newsize与allocated的关系满足newsize < allcated/2的时候,Python甚至还会通过realloc来收缩列表的内存空间,真是恨不得把一个字节掰成两个字节来用。
。Python的list操作非常灵活,支持一个很有趣的特性,就是负值索引,比如一个n个元素的list:lst[n],那么lst[-1]就是lst[n-1]。作为对灵活性的代价,Python对插入点的确定就不能像STL中vector那样直截了当,必须处理负数的情形。
可以看到,不管你插入在什么位置,对于Python来说,都是合法的,它会自己调整插入的位置。在确定了插入的位置之后,Python会在代码清单4-3的3处开始搬动元素,将插入点之后的所有元素向下挪动一个位置。
Python进行元素插入的动作流程如图4-4所示:
图4_4
值得注意的是,通过与vector类似的内存管理机制,现在,PyListObject的allocated已经变成10了,而ob_size却只有7。
在Python中,list还有另一个被广泛使用的插入操作append。这个操作与上面所描述的插入操作非常类似:

[listobject.c]//Python提供的C APIint PyList_Append(PyObject *op, PyObject *newitem){    if (PyList_Check(op) && (newitem != NULL))        return app1((PyListObject *)op, newitem);    return -1;}//与append对应的C函数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+1个位置上的(即list[ob_size]处),而不是第allocated个位置上。图4-5展示了append元素101之后的PyListObject对象:
图4_5
在app1中调用list_resize时,由于newsize(8)在5和10之间,所以不需要再分配内存空间。直接将101放置在第8个位置上即可。

4.2.4 删除元素

图4-6展示了一个使用PyListObject中删除元素功能的例子。
图4_6
当Python执行lst.remove(3)时,PyListObject中的listremove操作会被激活:

[listobject.c]static PyObject * listremove(PyListObject *self, PyObject *v){    int i;    for (i = 0; i < self->ob_size; i++) {        //比较list中的元素与待删除的元素v        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;}

Python会对整个列表进行遍历,在遍历PyListObject中所有元素的过程中,将待删除的元素与PyListObject中的每个元素一一进行比较,比较操作是通过PyObject_ RichCompareBool完成的,如果其返回值大于0,则表示列表中的某个元素与待删除的元素匹配。一旦在列表中发现匹配的元素,Python将立即调用list_ass_slice删除该元素。其函数原形如下:

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

list_ass_slice实际上并非是一个专用于删除操作函数,它的完整功能如下:

  • a[ilow:ihigh] = v if v != NULL
  • del a[ilow:ihigh] if v == NULL

可见,这个家伙实际上有着replace和remove两种语义,决定使用哪种语义的,正是最后一个参数v。图4-7展示了当Python内部调用这个函数时,会发生的动作。
图4_7
当执行l[1:3] = [‘a’, ‘b’]时,Python内部就调用了list_ass_slice,而其参数为ilow=1, ihigh=3, v=[‘a’, ‘b’]。 而当list_ass_slice的参数v为NULL时,Python就会将默认的replace语义替换为remove语义,删除[ilow, ihigh]范围内的元素,这正是listremove期望的动作。同样,在图4-7中,我们通过l[1:2] = []的执行看到了这一点,对于这个表达式语句,Python内部调用的正是list_ass_slice(l, 1, 2, NULL)。
对于list_ass_slice的具体实现,这里就不过多地深入了,有兴趣的读者可以参考Python的源代码。在list_ass_slice中,当进行元素删除动作时,实际上是通过memmove简单地搬移内存来实现的。这就意味着,当调用list的remove操作删除list中的元素时,一定会触发内存搬移的动作,这一点跟C++中的vector是完全一致的,而与C++中的list则完全不同。
图4_8

4.3 PyListObject对象缓冲池

[listobject.c]static void list_dealloc(PyListObject *op){    int i;    //[1] :销毁PyListObject对象维护的元素列表    if (op->ob_item != NULL) {        i = op->ob_size;        while (--i >= 0) {            Py_XDECREF(op->ob_item[i]);        }        PyMem_FREE(op->ob_item);    }    //[2] :释放PyListObject自身    if (num_free_lists < MAXFREELISTS && PyList_CheckExact(op))        free_lists[num_free_lists++] = op;    else        op->ob_type->tp_free((PyObject *)op);}

在创建一个新的list时,我们看到创建过程实际分离为两步,首先创建PyListObject对象,然后创建PyListObject对象所维护的元素列表。与之对应,在销毁一个list时,销毁的过程也是分离的,首先Python会在代码清单4-4的[1]处销毁PyListObject对象所维护的元素列表,然后在代码清单4-4的[2]处释放PyListObject对象自身。
在删除PyListObject对象自身时,Python会检查我们开始提到的那个缓冲池,free_lists,查看其中缓存的PyListObject的数量是否已经满了。如果没有,就将该待删除的PyListObject对象放到缓冲池中,以备后用。
现在一切真相大白了,那个在Python启动时空荡荡的缓冲池原来都是被本应该死去的PyListObject对象给填充了 ,在以后创建新的PyListObject的时候,Python会首先唤醒这些已经“死去”的PyListObject,又给它们一个重新做“人”的机会。但是,需要指出,这里缓冲的仅仅是PyListObject对象,而没有这个对象曾经拥有的PyObject*元素列表,因为这些PyObject*指针的引用计数已经减少了,这些指针所指的对象都要各奔前程,或生存,或毁灭,不再被PyListObject所给予的那个引用计数所束缚。PyListObject如果继续维护一个指向这些对象指针的列表,就可能产生悬空指针的问题。所以,PyObject*列表所占用的空间必须归还给系统。
当然,我们实际上可以将PyListObject对象所维护的元素列表保留,即在代码清单4-4的1处仅仅调整引用计数,并将列表中的各个元素都设置为NULL,却并不释放元素列表的内存空间。但是如此一来,这些已经被释放的内存并不会归还给系统堆,这就意味着除了PyListObject对象自身,没有人能再使用这些内存。虽然保留元素列表可以在创建list时免去创建元素列表的开销,但是Python为了避免过多消耗系统内存,采取了将元素列表的内存归还给系统堆的做法,以时间换取空间。
图4_9

4.4 Hack PyListObject

在PyListObject的输出操作list_print(python 3.5.2 源码里没有这个。。可以考虑用list_repr函数来hack)中,我们添加了如下代码,以观察PyList- Object对内存的管理:
图4_10
接下来我们观察一下PyListObject对象的创建和删除对于Python维护的PyListObject对象缓冲池的影响。
图4_11
这次为了消除Python交互环境执行时对PyListObject对象缓冲池的影响,我们通过执行py脚本文件来观察。从图4-11中可以看到,当创建新的PyListObject对象时,如果缓冲池中有可用的PyListObject对象,则会使用缓冲池中的对象;而在销毁一个PyListObject对象时,确实将这个对象放到了缓冲池中。

原创粉丝点击