Python中的List物件(《Python原始碼剖析》筆記四)

鬆直發表於2017-07-07

這是我的關於《Python原始碼剖析》一書的筆記的第四篇。Learn Python by Analyzing Python Source Code · GitBook

PyListObject是Python對列表的抽象,有的熟悉C++的人很可能自然而然地將Python中的list和C++ STL中的list對應起來。事實上這是不正確的,Python中的list更像C++ STL中的vector而不是list。在C++ STL中,list的實現是一個雙向連結串列,vector的實現是一個動態陣列。也就是說,Python中的list是一個動態陣列,它儲存在一個連續的記憶體塊中,隨機存取的時間複雜度是O(1),但插入和刪除時會造成記憶體塊的移動,時間複雜度是O(n)。同時,當陣列中記憶體不夠時,會重新申請一塊記憶體空間並進行記憶體拷貝。

PyListObject物件

在Python的列表中,無一例外地存放的都是PyObject*指標。所以實際上,我們可以這樣看待Python中的PyListObject:vector<PyObject*>。

顯然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有什麼關係呢?

前面我們提到,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)。知道這些,就可以在寫程式碼時選擇最適合自己需求的資料結構,從而優化效能。


相關文章