Python中l(wèi)ist的實現(xiàn)

原文鏈接
這篇文章介紹了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編程》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容