這是個人關於《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++
在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複製代碼
爲了建立一個列表,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會根據不一樣狀況執行操做:
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的建立時提到對象緩衝池的存在,也就是那個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)。知道這些,就能夠在寫代碼時選擇最適合本身需求的數據結構,從而優化性能。