Python list 实现原理

我们通过本文描述CPython实现 list 列表对象,Cpython是python最常用的实现。

在Python中List 功能非常强大,同时,它的实现方式也是非常有趣。

接下来,通过一个简单的python脚本演示,list中追加 integer 数据,并将它结果打印出来。

>>> l = []

>>> l.append(1)

>>> l.append(2)

>>> l.append(3)

>>> l

[1, 2, 3]

>>> for e in l:

... print e

...

1

2

3

正如你看到的样子,列表list是可迭代的(iterable)

List 数据结构

在CPython中列表List是通过C语言的结构体(structure)来表示。

ob_item是指向列表元素的指针列表。

allocated是在内存中分配的插槽数

typedef struct {

PyObject_VAR_HEAD

PyObject **ob_item;

Py_ssize_t allocated;

} PyListObject;

List 初始化过程

当一个空数组初始化时,发生了什么

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

重要的是要注意分配的插槽和列表大小之间的差异。 列表的大小与len(l)相同。 分配的插槽数是已在内存中分配的数量。 通常,您会看到分配的大小可能大于size。 这是为了避免每次将新元素添加到列表时都需要调用realloc。 稍后我们将详细介绍。

Append

当我们建一个integer追加到list中,l.append(1),将会发生什么? 一个C内部方法 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太多时间。 列表的增长方式为: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

现在分配了4个slot来包含元素,第一个slot是整数1。您可以在下图中看到l [0]指向我们刚刚附加的整数对象。 虚线正方形表示已分配但尚未使用的插槽。

Appand 操作的时间复杂度 O(1)

image

我们继续添加一个元素:l.append(2)。 使用n + 1 = 2调用list_resize,但是由于分配的大小为4,因此无需分配更多的内存。 当我们再添加2个整数时,也会发生同样的事情:l.append(3),l.append(4)。 下图显示了到目前为止的内容。

image

Insert

让我们在位置1处插入一个新的整数(5):l.insert(1,5),看看内部发生了什么。 函数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

image

虚线正方形表示已分配但尚未使用的插槽。 在这里,分配了8个插槽,但列表的大小或长度仅为5。

Insert 操作的复杂度为O(n)

Pop

当您弹出最后一个元素:l.pop()时,将调用listpop()。 在listpop()内部调用list_resize,如果新大小小于分配大小的一半,则列表将缩小。

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 操作的复杂度为 O(1).

image

您可以看到插槽4仍指向整数,但重要的是列表的大小现在为4。

让我们再弹出一个元素。 在list_resize()中,size – 1 = 4 – 1 = 3小于分配的插槽的一半,因此列表缩小为6个插槽,并且列表的新大小现在为3。

您可以观察到插槽3和4仍指向一些整数,但重要的是列表的大小现在为3。

image

Remove

Python列表对象具有删除特定元素的方法:l.remove(5)。底层函数 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_ass_slice(),很有趣的是它如何工作。 在这里,低偏移量是1,高偏移量是2,因为我们要删除位置1处的元素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操作的复杂度为O(n).

image