原文鏈接
這篇文章介紹了Python中l(wèi)ist是如何實現(xiàn)的。
在Python中l(wèi)ist特別有用。讓我們來看下list的內(nèi)部是如何實現(xiàn)的。
來看下面簡單的程序,在list中添加一些整數(shù)并將他們打印出來。
>>> L = []
>>> L.append(1)
>>> L.append(2)
>>> L.append(3)
>>> L
[1, 2, 3]
>>> for e in L:
... print e
...
1
2
3
正如你所看到的,list是可以迭代的。
List對象的C結(jié)構(gòu)
Python中l(wèi)ist是用下邊的C語言的結(jié)構(gòu)來表示的。ob_item
是用來保存元素的指針數(shù)組,allocated是ob_item
預(yù)先分配的內(nèi)存總?cè)萘?/p>
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
List的初始化
讓我們來看下當(dāng)初始化一個空list的時候發(fā)生了什么 L = []
arguments: size of the list = 0
returns: list object = []
PyListNew:
nbytes = size * size of global Python object = 0
allocate new list object
allocate list of pointers (ob_item) of size nbytes = 0
clear ob_item
set list's allocated var to 0 = 0 slots
return list object
非常重要的是知道list申請內(nèi)存空間的大?。ê笪挠胊llocated代替)的大小和list實際存儲元素所占空間的大小(ob_size
)之間的關(guān)系,ob_size
的大小和len(L)
是一樣的,而allocated的大小是在內(nèi)存中已經(jīng)申請空間大小。通常你會看到allocated的值要比ob_size
的值要大。這是為了避免每次有新元素加入list時都要調(diào)用realloc進行內(nèi)存分配。接下來我們會看到更多關(guān)于這些的內(nèi)容。
Append
我們在list中追加一個整數(shù):L.append(1)。發(fā)生了什么?調(diào)用了內(nèi)部的C函數(shù)app1()
arguments: list object, new element
returns: 0 if OK, -1 if not
app1:
n = size of list
call list_resize() to resize the list to size n+1 = 0 + 1 = 1
list[n] = list[0] = new element
return 0
來讓我們看下list_resize()
,list_resize()
會申請多余的空間以避免調(diào)用多次list_resize()
函數(shù),list增長的模型是:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …
arguments: list object, new size
returns: 0 if OK, -1 if not
list_resize:
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) = 3
new_allocated += newsize = 3 + 1 = 4
resize ob_item (list of pointers) to size new_allocated
return 0
開辟了四個內(nèi)存空間來存放list中的元素,存放的第一個元素是1。你可以從下圖中看到L[0]指向了我們剛剛加進去的元素。虛線的框代表了申請了但是還沒有使用(存儲元素)的內(nèi)存空間

我們繼續(xù)加入一個元素:L.append(2)。調(diào)用
list_resize
,同時n+1=2。但是因為allocated(譯者注:已經(jīng)申請的空間大?。┦?。所以沒有必要去申請新的內(nèi)存空間。相同的事情發(fā)生在再次在list中添加兩個元素的時候: L.append(3),L.append(4)。下圖展示了到目前為止我們做了什么。
Insert
現(xiàn)在我們在列表的第一個位置插入一個整數(shù)5:L.insert(1, 5),看看內(nèi)部發(fā)生了什么。調(diào)用了ins1()
arguments: list object, where, new element
returns: 0 if OK, -1 if not
ins1:
resize list to size n+1 = 5 -> 4 more slots will be allocated
starting at the last element up to the offset where, right shift each element
set new element at offset where
return 0

虛線框表示已經(jīng)申請但是沒有使用的內(nèi)存。申請了8個內(nèi)存空間但是list實際用來存儲元素只使用了其中5個內(nèi)存空間
insert的時間復(fù)雜度是O(n)
Pop
當(dāng)你彈出list的最后一個元素:L.pop()。調(diào)用listpop(),list_resize
在函數(shù)listpop()內(nèi)部被調(diào)用,如果這時ob_size
(譯者注:彈出元素后)小于allocated(譯者注:已經(jīng)申請的內(nèi)存空間)的一半。這時申請的內(nèi)存空間將會縮小。
arguments: list object
returns: element popped
listpop:
if list empty:
return null
resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage
set list object size to 4
return last element
Pop的時間復(fù)雜度是O(1)

你可以發(fā)現(xiàn)4號內(nèi)存空間指向還指向那個數(shù)值(譯者注:彈出去的那個數(shù)值),但是很重要的是
ob_size
現(xiàn)在卻成了4.讓我們再彈出一個元素。在
list_resize
內(nèi)部,size – 1 = 4 – 1 = 3 比allocated(已經(jīng)申請的空間)的一半還要小。所以list的申請空間縮小到6個,list的實際使用空間現(xiàn)在是3個(譯者注:根據(jù)(newsize >> 3) + (newsize < 9 ? 3 : 6) = 3在文章最后有詳述)
你可以發(fā)現(xiàn)(下圖)3號和4號內(nèi)存空間還存儲著一些整數(shù),但是list的實際使用(存儲元素)空間卻只有3個了。

Remove
Python list對象有一個方法可以移除一個指定的元素。調(diào)用listremove()。
arguments: list object, element to remove
returns none if OK, null if not
listremove:
loop through each list element:
if correct element:
slice list between element's slot and element's slot + 1
return none
return null
切開list和刪除元素,調(diào)用了list_ass_slice()
(譯者注:在上文slice list between element's slot and element's slot + 1被調(diào)用),來看下list_ass_slice()
是如何工作的。在這里,低位為1 高位為2(譯者注:傳入的參數(shù)),我們移除在1號內(nèi)存空間存儲的數(shù)據(jù)5
arguments: list object, low offset, high offset
returns: 0 if OK
list_ass_slice:
copy integer 5 to recycle list to dereference it
shift elements from slot 2 to slot 1
resize list to 5 slots
return 0
Remove的時間復(fù)雜度為O(n)

譯者注:
文中l(wèi)ist的sort部分沒有進行翻譯
核心部分
我們能看到 Python 設(shè)計者的苦心。在需要的時候擴容,但又不允許過度的浪費,適當(dāng)?shù)膬?nèi)存回收是非常必要的。
這個確定調(diào)整后的空間大小算法很有意思。
調(diào)整后大小 (new_allocated) = 新元素數(shù)量 (newsize) + 預(yù)留空間 (new_allocated)
調(diào)整后的空間肯定能存儲 newsize 個元素。要關(guān)注的是預(yù)留空間的增長狀況。
將預(yù)留算法改成 Python 版就更清楚了:(newsize // 8) + (newsize < 9 and 3 or 6)。
當(dāng) newsize >= allocated,自然按照這個新的長度 "擴容" 內(nèi)存。
而如果 newsize < allocated,且利用率低于一半呢?
allocated newsize new_size + new_allocated
10 4 4 + 3
20 9 9 + 7
很顯然,這個新長度小于原來的已分配空間長度,自然會導(dǎo)致 realloc 收縮內(nèi)存。(不容易啊)
引自《深入Python編程》