Python中的List对象(《Python源码剖析》笔记四)

这是个人关于《Python源码剖析》一书的笔记的第四篇。Learn Python by Analyzing Python Source Code · GitBook
python

PyListObject是Python对列表的抽象,有的熟悉C++的人极可能天然而然地将Python中的list和C++ STL中的list对应起来。事实上这是不正确的,Python中的list更像C++ STL中的vector而不是list。在C++ STL中,list的实现是一个双向链表,vector的实现是一个动态数组。也就是说,Python中的list是一个动态数组,它储存在一个连续的内存块中,随机存取的时间复杂度是O(1),但插入和删除时会形成内存块的移动,时间复杂度是O(n)。同时,当数组中内存不够时,会从新申请一块内存空间并进行内存拷贝。c++

PyListObject对象

在Python的列表中,无一例外地存放的都是PyObject*指针。因此实际上,咱们能够这样看待Python中的PyListObject:vector<PyObject*>。git

显然PyListObject是一个变长对象,同时它还支持插入和删除等操做,因此它仍是一个可变对象。数组

咱们先来看一看PyListObject的定义:缓存

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

如咱们所料,PyListObject的头部就是一个PyObject_VAR_HEAD,随后是一个类型为PyObject ** 的指针,这个指针和后面的allocated就是维护元素列表的关键。指针指向了元素列表所在内存块的首地址,而allocated中则维护了当前列表中的可容纳的元素的总数。数据结构

还记得吗?PyObject_VAR_HEAD中有一个ob_size,它表明着变长对象中元素的数量。那么它和allocated有什么关系呢?app

前面咱们提到,Python中的list是一个动态数组。因此,在每一次须要申请内存时,PyListObject就会申请一大块内存,这时申请内存的总大小记录在allocated中,而实际被使用了的内存的数量则记录在ob_size中。函数

因此,咱们就能够获得,对于一个PyListObject,必定有下列关系:性能

0<= ob_size <= allocatedlen(list) == ob_sizeob_item == NULL implies ob_size == allocated == 0复制代码

PyListObject对象的建立和维护

建立对象

为了建立一个列表,Python只提供了一条途径——PyList_New。这个函数接受一个size参数,从而容许咱们指定该列表初始的元素个数。不过咱们这里只能指定元素个数,不能指定元素是什么。优化

[listobject.c]PyObject *PyList_New(Py_ssize_t size){PyListObject *op;#ifdef SHOW_ALLOC_COUNTstatic int initialized = 0;if (!initialized) {Py_AtExit(show_alloc);initialized = 1;}#endif
if (size < 0) {PyErr_BadInternalCall();return NULL;}if (numfree) {numfree--;op = free_list[numfree];_Py_NewReference((PyObject *)op);#ifdef SHOW_ALLOC_COUNTcount_reuse++;#endif} else {op = PyObject_GC_New(PyListObject, &PyList_Type);if (op == NULL)return NULL;#ifdef SHOW_ALLOC_COUNTcount_alloc++;#endif}if (size <= 0)op->ob_item = NULL;else {op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));if (op->ob_item == NULL) {Py_DECREF(op);return PyErr_NoMemory();}}Py_SIZE(op) = size;op->allocated = size;_PyObject_GC_TRACK(op);return (PyObject *) op;}复制代码

首先,Python会计算须要的内存总量,由于PyList_New指定的只是元素的个数,而不是元素实际将占用的内存空间。在这里,Python会检查制定的元素个数是否会大到使所需内存数量产生溢出的程度,若是会溢出,那么Python将不会进行任何动做。

咱们能够清楚的看到,Python中的列表对象其实是分红两部分的,一是PyListObject对象自己,一是PyListObject维护的元素列表。这是两块分离的内存,它们经过ob_item创建了联系。

在建立PyListObject时,首先会检查缓冲池free_lists中是否有可用的对象,若是有,就直接使用这个对象,若是没有,则会调用PyObject_GC_New在系统堆中申请内存,建立新的PyListObject对象。

设置元素

当咱们经过PyList_New()建立一个PyListObject时,咱们并无设置元素的值,这个操做须要调用PyList_SetItem():

[listobject.c]int PyList_SetItem(PyObject *op, Py_ssize_t i,PyObject *newitem){PyObject **p;if (!PyList_Check(op)) {Py_XDECREF(newitem);PyErr_BadInternalCall();return -1;}if (i < 0 || i >= Py_SIZE(op)) {Py_XDECREF(newitem);PyErr_SetString(PyExc_IndexError,"list assignment index out of range");return -1;}p = ((PyListObject *)op) -> ob_item + i;Py_XSETREF(*p, newitem);return 0;}复制代码

首先Python会进行类型检查,而后进行索引有效性检查,都顺利经过后,Python将新的对象的指针放到指定的位置,同时将原来的对象的引用计数减一。

插入元素

设置元素和插入元素的动做不一样,前者不会致使ob_item指向的内存发生变化,然后者则有可能使其发生变化。

[listobject.c]int PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem){if (!PyList_Check(op)) {PyErr_BadInternalCall();return -1;}return ins1((PyListObject *)op, where, newitem);}static int ins1(PyListObject *self, Py_ssize_t where, PyObject *v){Py_ssize_t i, n = Py_SIZE(self);PyObject **items;if (v == NULL) {PyErr_BadInternalCall();return -1;}if (n == PY_SSIZE_T_MAX) {PyErr_SetString(PyExc_OverflowError,"cannot add more objects to list");return -1;}
if (list_resize(self, n+1) < 0)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;}复制代码

为了完成元素的插入,必须知足一个条件,那就是要有足够的内存来保存这些元素。Python经过调用list_resize来保证该条件成立。

static int list_resize(PyListObject *self, Py_ssize_t newsize){PyObject **items;size_t new_allocated;Py_ssize_t allocated = self->allocated;
if (allocated >= newsize && newsize >= (allocated >> 1)) {assert(self->ob_item != NULL || newsize == 0);Py_SIZE(self) = newsize;return 0;}
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
/* check for integer overflow */if (new_allocated > SIZE_MAX - newsize) {PyErr_NoMemory();return -1;} else {new_allocated += newsize;}
if (newsize == 0)new_allocated = 0;items = self->ob_item;if (new_allocated <= (SIZE_MAX / sizeof(PyObject *)))PyMem_RESIZE(items, PyObject *, new_allocated);elseitems = NULL;if (items == NULL) {PyErr_NoMemory();return -1;}self->ob_item = items;Py_SIZE(self) = newsize;self->allocated = new_allocated;return 0;}复制代码

Python会根据不一样状况执行操做:

  • newsize < allocated && newsize >allocated/2:简单调整ob_size
  • 其余状况下从新分配内存空间

python不只在内存不够用的时候会给PyListObject分配更多的内存,当newsize < allocated/2时,还会收缩内存空间,以达到内存利用的最大化。

删除元素

在一个列表中删除元素,Python会调用listremove:

static PyObject *listremove(PyListObject *self, PyObject *v){Py_ssize_t i;
for (i = 0; i < Py_SIZE(self); 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;}复制代码

Python会对整个列表进行遍历,在遍历的过程当中将要插入的元素和列表中的元素比较,若是发现有匹配的元素,则当即删除该元素。

PyListObject对象缓冲池

咱们在说PyListObject的建立时提到对象缓冲池的存在,也就是那个free_lists数组,那么它里面的PyListObject是从哪来的呢?

根据前面的经验,应该就是在对象删除的时候暗藏玄机。

static void list_dealloc(PyListObject *op){Py_ssize_t i;PyObject_GC_UnTrack(op);Py_TRASHCAN_SAFE_BEGIN(op)if (op->ob_item != NULL) {i = Py_SIZE(op);while (--i >= 0) {Py_XDECREF(op->ob_item[i]);}PyMem_FREE(op->ob_item);}if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))free_list[numfree++] = op;elsePy_TYPE(op)->tp_free((PyObject *)op);Py_TRASHCAN_SAFE_END(op)}复制代码

再删除PyListObject对象时,python会检查free_lists中缓存的对象是否已满,若是没有就将该待删除的对象放到缓冲池中。不过这里缓存的只是PyListObject对象,而不是这个对象曾经维护的PyObject *元素列表,由于这些对象的引用计数已经减小。

小结

了解list的底层实现或许没有太多做用,最重要的就是要了解在开头写的,list随机存取的时间复杂度是O(1),但插入查找和删除的时间复杂度是O(n)。知道这些,就能够在写代码时选择最适合本身需求的数据结构,从而优化性能。

相关文章
相关标签/搜索