楔子
Python中的列表可以說使用的非常廣泛了,在初學列表的時候,老師會告訴你列表就是一個大倉庫,什么都可以存放。不過在最開始的幾個章節中,我們花了很大的筆墨介紹了Python中的對象,並明白了Python中變量的本質,我們知道列表中存放的元素其實都是泛型指針PyObject *,所以到現在列表已經沒有什么好神秘的了。
並且根據我們使用列表的經驗,我們可以得出以下兩個結論:
每個列表中的元素個數可以不一樣:所以這是一個變長對象
可以對列表中的元素進行添加、刪除、修改等操作,所以這是一個可變對象
在分析列表對應的底層結構之前,我們先來回顧一下列表的使用。
# 創建一個列表,這里是通過Python/C API創建的
>>> lst = [1, 2, 3, 4]
>>> lst
[1, 2, 3, 4]
# 往列表尾部追加一個元素,此時是在本地操作的,返回值為None
# 但是列表被改變了
>>> lst.append(5)
>>> lst
[1, 2, 3, 4, 5]
# 從尾部彈出一個元素,會返回彈出的元素
>>> lst.pop()
5
# 此時列表也會被修改
>>> lst
[1, 2, 3, 4]
# 另外在pop的時候還可以指定索引,彈出指定的元素
>>> lst.pop(0)
1
>>> lst
[2, 3, 4]
# 也可以在指定位置插入一個元素
>>> lst.insert(0, 'x')
>>> lst
['x', 2, 3, 4]
# 通過extend在尾部追加多個元素
>>> lst.extend([7, 8, 9])
>>> lst
['x', 2, 3, 4, 7, 8, 9]
# 查找指定元素第一次出現的位置
>>> lst.index(3)
2
# 計算某個元素在列表中出現的次數
>>> lst.count(3)
2
# 翻轉列表
>>> lst.reverse()
>>> lst
[9, 8, 7, 4, 3, 2, 'x']
# 根據元素的值刪除第一個出現的元素
>>> lst.remove(4)
[9, 8, 7, 3, 2, 'x']
# 清空列表
>>> lst.clear()
>>> lst
[]
>>>
上面的一些操作是列表經常使用的,但是在分析它的實現之前,我們肯定要了解它們的時間復雜度如何。這些東西即使不看源碼,也是必須要知道的,尤其想要成為一名優秀的Python工程師。
append:會向尾部追加元素,所以時間復雜度為O(1)
pop:默認從尾部彈出元素,所以時間復雜度為O(1);如果不是尾部,而是從其它的位置彈出元素的話,那么該位置后面所有的元素都要向前移動,此時時間復雜度為O(n)
insert:向指定位置插入元素,該位置后面的所有元素都要向后移動,所以時間復雜度為O(n)
注意:由於列表里面的元素個數是可以自由變化的,所以列表有一個容量的概念,我們后面會說。當添加元素時,列表可能會擴容;同理當刪除元素時,列表可能會縮容。
下面我們就來看一下列表對應的底層結構。
列表的內部結構--PyListObject
list 對象(列表)
在 Python 內部,由 PyListObject 結構體表示,定義於頭文件 Include/listobject.h 中:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
我們看到里面有如下成員:
PyObject_VAR_HEAD: 變長對象的公共頭部信息
ob_item:一個二級指針,指向一個PyObject *類型的指針數組,這個指針數組保存的便是對象的指針,而操作底層數組都是通過ob_item來進行操作的。
allocated:容量, 我們知道列表底層是使用了C的數組, 而底層數組的長度就是列表的容量
列表之所以要有容量的概念,是因為列表可以動態添加元素,但是底層的數組在創建完畢之后,其長度卻是固定的。所以一旦添加新元素的時候,發現底層數組已經滿了,這個時候只能申請一個更長的數組,同時把原來數組中的元素依次拷貝到新的數組里面(這一過程就是列表的擴容)
,然后再將新元素添加進去。但是問題來了,總不可能每添加一個元素,就申請一次數組、將所有元素都拷貝一次吧。所以Python在列表擴容的時候,會將底層數組申請的長一些,可以在添加元素的時候不用每次都申請新的數組。
這便是列表的底層結構示意圖,圖中的object只是單純的代指對象,不是Python中的基類object。我們看到底層數組的長度為5,說明此時列表的容量為5,但是里面只有3個PyObject *指針,說明列表的ob_size是3,或者說列表里面此時有3個元素。注意:盡管底層數組的容量目前是5個,但是我們訪問的時候,最多只能訪問到第三個元素,也就是說索引最大只能是2,這是顯而易見的,因為列表里面只有3個元素。
如果這個時候我們往列表中append一個元素,那么會將這個新元素設置在底層數組中索引為ob_size的位置、或者說第四個位置。一旦設置完,ob_size會自動加1,因為ob_size要和列表的長度保持一致。
如果此時再往列表中append一個元素的話,那么還是將新元素設置在索引為ob_size的位置,此時也就是第5個位置。
列表的容量是5,但此時長度也達到了5,這說明當下一次append的時候已經沒有辦法再容納新的元素了。因為此時列表的長度、或者說元素個數已經達到了容量,當然最直觀的還是這里的底層數組,很明顯全都占滿了。那這個時候如果想再接收新的元素的話,要怎么辦呢?顯然只能擴容了。
原來的容量是5個,長度也是5個,當再來一個新元素的時候由於沒有位置了,所以要擴容。但是擴容的時候肯定會將容量申請的大一些、即底層數組申請的長一些(具體申請多長,Python內部有一個公式,我們后面會說)
,假設申請的新的底層數組長度是8,那么說明列表的容量就變成了8。然后將原來數組中的PyObject *按照順序依次拷貝到新的數組里面,再讓ob_item指向新的數組。最后將要添加的新元素設置在新的數組中索引為ob_size的位置、即第6個位置,然后將ob_size加1,此時ob_size就變成了6。
以上便是列表底層在擴容的時候所經歷的過程。
由於擴容會申請新的數組,然后將舊數組的元素拷貝到新數組中,所以這是一個時間復雜度為O(n)的操作。而append可能會導致列表擴容,因此append最壞情況下也是一個O(n)的操作,只不過擴容不會頻繁發生,所以append的平均時間復雜度還是O(1)。
另外我們還可以看到一個現象,那就是Python中的列表在底層是分開存儲的,因為PyListObject結構體實例並沒有存儲相應的指針數組,而是存儲了指向這個指針數組的二級指針。顯然我們添加、刪除、修改元素等操作,都是通過ob_item這個二級指針來間接操作這個指針數組。
所以底層對應的PyListObject實例的大小其實是不變的,因為指針數組沒有存在PyListObject里面。但是Python在計算內存大小的時候是會將這個指針數組也算進去的,所以Python中列表的大小是可變的。
而且我們知道,列表在append之后地址是不變的,至於原因上面的幾張圖已經解釋的很清楚了。如果長度沒有達到容量,那么append其實就是往底層數組中設置了一個新元素;如果達到容量了,那么會擴容,但是擴容只是申請一個新的指針數組,然后讓ob_item重新指向罷了。所以底層數組會變,但是PyListObject結構體實例本身是沒有變化的。因此列表無論是append、extend、pop、insert等等,只要是在本地操作,那么它的地址是不會變化的。
下面我們再來看看Python中的列表所占的內存大小是怎么算的:
PyObject_VAR_HEAD: 24字節
ob_item: 二級指針, 8字節
allocated: 8字節
但是不要忘記,在計算列表大小的時候,ob_item指向的指針數組也要算在內。所以:一個列表的大小 = 40 + 8 * 指針數組長度(或者列表容量)
。注意是底層數組長度、或者列表容量,可不是列表長度,因為底層數組一旦申請了,不管你用沒用,大小就擺在那里了。就好比你租了間房子,就算不住,房租該交還是得交。
# 顯然一個空數組占40個字節
print([].__sizeof__()) # 40
# 40 + 3 * 8 = 64
print([1, 2, "x" * 1000].__sizeof__()) # 64
# 雖然里面有一個長度為1000的字符串,但我們說列表存放的都是指針, 所以大小都是8字節
# 注意: 我們通過l = [1, 2, 3]這種方式創建列表的話
# 不管內部元素有多少個, 其ob_size和allocated都是一樣的
# 那么列表什么時候會擴容呢? 答案是在添加元素的時候發現容量不夠了才會擴容
lst = list(range(10))
# 40 + 10 * 8 = 120
print(lst.__sizeof__()) # 120
# 這個時候append一個元素
lst.append(123)
print(lst.__sizeof__()) # 184
# 我們發現大小達到了184, (184 - 40) // 8 = 18, 說明擴容之后申請的底層數據的長度為18
所以列表的大小我們就知道是怎么來的了,而且為什么列表在通過索引定位元素的時候,時間復雜度是O(1)。因為列表中存儲的都是對象的指針,不管對象有多大,其指針大小是固定的,都是8字節。通過索引可以瞬間計算出偏移量,從而找到對應元素的指針,而操作指針會自動操作指針所指向的內存。
print([1, 2, 3].__sizeof__()) # 64
print([[1, 2, 3]].__sizeof__()) # 48
相信上面這個結果,你肯定能分析出原因。因為第一個列表中有3個指針,所以是40 + 24 = 64;而第二個列表中有一個指針,所以是40 + 8 = 48。用一張圖來展示一下[1, 2, 3]
和[[1, 2, 3]]
的底層結構,看看它們之間的區別:
分析完PyListObject之后,我們來看看它支持的操作,顯然我們要通過類型對象PyList_Type來查看。
PyTypeObject PyList_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list",
sizeof(PyListObject),
0,
(destructor)list_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)list_repr, /* tp_repr */
0, /* tp_as_number */
&list_as_sequence, /* tp_as_sequence */
&list_as_mapping, /* tp_as_mapping */
//......
};
我們看到,列表支持序列型操作和映射型操作,下面我們就來分析一下。
列表支持的操作
我們看看平常使用的列表所支持的操作在底層是如何實現的。
自動擴容
我們先來說說列表的擴容,因為我們知道列表是會自動擴容的,那么什么時候會擴容呢?我們說列表擴容的時候,是在添加元素時發現底層數組已經滿了的情況下才會擴容。換句話說,一個列表在添加元素的時候會擴容,那么說明在添加元素之前,其內部的元素個數和容量是相等的。然后我們看看底層是怎么實現的,這些操作都位於Objects/listobject.c中。
static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{ //參數self就是列表啦,newsize指的元素在添加之后的ob_size
//比如列表的ob_size是5,那么在append的時候發現容量不夠,所以會擴容,那么這里的newsize就是6
//如果是extend添加3個元素,那么這里的newsize就是8
//當然list_resize這個函數不僅可以擴容,也可以縮容,假設列表原來有1000個元素,這個時候將列表清空了
//那么容量肯定縮小,不然會浪費內存,如果清空了列表,那么這里的newsize顯然就是0
//items是一個二級指針,顯然是用來指向指針數組的
PyObject **items;
//新的容量,以及對應的內存大小
size_t new_allocated, num_allocated_bytes;
//獲取原來的容量
Py_ssize_t allocated = self->allocated;
//如果newsize達到了容量的一半,但還沒有超過容量, 那么意味着newsize、或者新的ob_size和容量是匹配的,所以不會變化
if (allocated >= newsize && newsize >= (allocated >> 1)) {
assert(self->ob_item != NULL || newsize == 0);
//只需要將列表的ob_size設置為newsize即可
Py_SIZE(self) = newsize;
return 0;
}
//走到這里說明容量和ob_size不匹配了,所以要進行擴容或者縮容。
//因此要申請新的底層數組,申請多少個?這里給出了公式,一會兒我們可以通過Python進行測試
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
//顯然容量不可能無限大,是有范圍的,當然這個范圍基本上是達不到的
if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
PyErr_NoMemory();
return -1;
}
//如果newsize為0,那么容量也會變成0,假設將列表全部清空了,容量就會變成0
if (newsize == 0)
new_allocated = 0;
//我們說數組中存放的都是PyObject *, 所以要計算內存
num_allocated_bytes = new_allocated * sizeof(PyObject *);
//申請相應大小的內存,將其指針交給items
items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
if (items == NULL) {
//如果items是NULL, 代表申請失敗
PyErr_NoMemory();
return -1;
}
//然后讓ob_item = items, 也就是指向新的數組, 此時列表就發生了擴容或縮容
self->ob_item = items;
//將ob_size設置為newsize, 因為它維護列表內部元素的個數
Py_SIZE(self) = newsize;
//將原來的容量大小設置為新的容量大小
self->allocated = new_allocated;
return 0;
}
我們看到還是很簡單的,沒有什么黑科技,下面我們就來分析一下列表擴容的時候,容量和元素個數之間的規律。其實在list_resize函數中是有注釋的,其種一行寫着:The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
說明我們往一個空列表中不斷append元素的時候,容量會按照上面的規律進行變化,我們來試一下。
# 還記得底層是怎么改變容量的嗎?
# 我們說有一個公式: new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
# 我們來看一下
lst = []
allocated = 0
print("此時容量是: 0")
for item in range(100):
lst.append(item) # 添加元素
# 計算ob_size
ob_size = len(lst)
# 判斷ob_size和當前的容量
if ob_size > allocated:
# lst的大小減去空列表的大小, 再除以8顯然就是容量的大小, 因為不管你有沒有用, 容量已經分配了
allocated = (lst.__sizeof__() - [].__sizeof__()) // 8
print(f"列表擴容啦, 新的容量是: {allocated}")
"""
此時容量是: 0
列表擴容啦, 新的容量是: 4
列表擴容啦, 新的容量是: 8
列表擴容啦, 新的容量是: 16
列表擴容啦, 新的容量是: 25
列表擴容啦, 新的容量是: 35
列表擴容啦, 新的容量是: 46
列表擴容啦, 新的容量是: 58
列表擴容啦, 新的容量是: 72
列表擴容啦, 新的容量是: 88
列表擴容啦, 新的容量是: 106
Process finished with exit code 0
"""
我們看到和官方給的結果是一樣的,顯然這是毫無疑問的,我們根據底層的公式也能算出來。
ob_size = 0
allocated = 0
print(allocated, end=" ")
for item in range(100):
ob_size += 1
if ob_size > allocated:
allocated = ob_size + (ob_size >> 3) + (3 if ob_size < 9 else 6)
print(allocated, end=" ")
# 0 4 8 16 25 35 46 58 72 88 106
但還是那句話,擴容是指解釋器發現容量不夠的情況下才會擴容,如果我們直接通過lst = []這種形式創建列表的話,那么其長度和容量是一樣的。
lst = [0] * 1000
# 長度和容量一致
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 1000 1000
# 但此時添加一個元素的話, 那么ob_size會變成1001, 大於容量1000
# 所以此時列表就要擴容了, 執行list_resize, 里面的new_size就是1001, 然后是怎么分配容量來着
# new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
print("新容量:", 1001 + (1001 >> 3) + (3 if 1001 < 9 else 6)) # 新容量: 1132
# append一個元素,列表擴容
lst.append(123)
# 計算容量
print((lst.__sizeof__() - [].__sizeof__()) // 8) # 1132
# 結果是一樣的, 因為底層就是這么實現的, 所以結果必須一樣
# 只不過我們通過這種測試的方式證明了這一點, 也更加了解了底層的結構是什么樣子的。
介紹完擴容,那么介紹縮容,因為列表元素個數要是減少到和容量不匹配的話,也要進行縮容。
舉個生活中的例子,假設你租了10間屋子用於辦公,顯然你要付10間屋子的房租,不管你有沒有住,一旦租了肯定是要付錢的。同理底層數組也是一樣,只要你申請了,不管有沒有元素,內存已經占用了。但有一天你用不到10間屋子了,假設會用8間或者9間,那么會讓剩余的屋子閑下來。但由於退租比較麻煩,並且只閑下來一兩間屋子,所以多余的屋子就不退了,還是會付10間屋子的錢,這樣當沒准哪天又要用的時候就不用重新租了。對於列表也是如此,如果在刪除元素(相當於屋子不用了)的時候發現長度沒有超過容量但是又達到了容量的一半,所以也不會縮容。但是,如果屋子閑了8間,也就是只需要兩間屋子就足夠了,那么此時肯定要退租了,閑了8間,可能會退掉6間。
lst = [0] * 1000
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 1000 1000
# 刪除500個元素, 此時長度或者說ob_size就為500
lst[500:] = []
# 但是ob_size還是達到了容量的一半, 所以不會縮容
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 500 1000
# 如果再刪除一個元素的話, 那么不好意思, 顯然就要進行縮容了, 因為ob_size變成了499, 小於1000 // 2
# 縮容之后容量怎么算呢? 還是之前那個公式
print(499 + (499 >> 3) + (3 if 499 < 9 else 6)) # 567
# 測試一下, 刪除一個元素, 看看會不會按照我們期待的規則進行縮容
lst.pop()
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 499 567
一切都和我們想的是一樣的,另外在代碼中我們還看到一個if語句,就是如果newsize是0,那么容量也是0,我們來測試一下。
lst = [0] * 1000
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 1000 1000
lst[:] = []
print(len(lst), (lst.__sizeof__() - [].__sizeof__()) // 8) # 0 0
# 如果按照之前的容量變化公式的話, 會發現結果應該是3, 但是結果是0, 就是因為多了if判斷:如果newsize是0, 就把容量也設置為0
print(0 + (0 >> 3) + (3 if 0 < 9 else 6)) # 3
# 但為什么要這么做呢?因為Python認為, 列表長度為0的話,說明你不想用這個列表了, 所以多余的3個也沒有必要申請了
# 我們還以租房為栗, 如果你一間屋子都不用了, 說明可能你不用這里的屋子辦公了
# 因此多余3間屋子也沒有必要再租了, 所以直接全部退掉
以上就是列表在改變容量時所采用的策略,我們從頭到尾全部分析了一遍。
append追加元素
append方法用於像尾部追加一個元素,我們看看底層實現。
static PyObject *
list_append(PyListObject *self, PyObject *object)
{
//顯然調用的app1是核心, 它里面實現了添加元素的邏輯
//Py_RETURN_NONE是一個宏,表示返回Python中的None, 因為list.append返回的就是None
if (app1(self, object) == 0)
Py_RETURN_NONE;
return NULL;
}
static int
app1(PyListObject *self, PyObject *v)
{ //self是列表,v是要添加的對象
//獲取列表的長度
Py_ssize_t n = PyList_GET_SIZE(self);
assert (v != NULL);
//如果長度已經達到了限制,那么無法再添加了, 會拋出OverflowError
if (n == PY_SSIZE_T_MAX) {
PyErr_SetString(PyExc_OverflowError,
"cannot add more objects to list");
return -1;
}
//還記得這個list_resize嗎? self就是列表, n + 1就是newsize,或者說新的ob_size
//會自動判斷是否要進行擴容, 當然里面還有重要的一步,就是將列表的ob_size設置成newsize、也就是這里的n + 1
//因為append之后列表長度大小會變化,而ob_size則要時刻維護這個大小
if (list_resize(self, n+1) < 0)
return -1;
//因為v作為了列表的一個元素,所以其指向的對象的引用計數要加1
Py_INCREF(v);
//然后調用PyList_SET_ITEM,這是一個宏,它的作用就是設置元素的,我們下面會看這個宏長什么樣
//原來的列表長度為n, 里面的元素的最大索引是n - 1,那么追加的話就等於將元素設置在索引為n的地方
PyList_SET_ITEM(self, n, v);
return 0;
}
//我們說PyList_SET_ITEM是用來設置元素的,設置在什么地方呢?顯然是設置在底層數組中
//PyList_SET_ITEM一個宏,除了這個宏之外,還有很多其它的宏,它們位於Inlcude/listobject.h中
#define PyList_GET_ITEM(op, i) (((PyListObject *)(op))->ob_item[i])
#define PyList_SET_ITEM(op, i, v) (((PyListObject *)(op))->ob_item[i] = (v))
#define PyList_GET_SIZE(op) (assert(PyList_Check(op)),Py_SIZE(op))
//這些宏的作用是啥,一目了然
獲取元素
我們在使用列表的時候,可以通過val = lst[1]這種方式獲取元素,那么底層是如何實現的呢?
static PyObject *
list_subscript(PyListObject* self, PyObject* item)
{
//先看item是不是一個整型,顯然這個item除了整型之外,也可以是切片
if (PyIndex_Check(item)) {
Py_ssize_t i;
//這里檢測i是否合法,因為Python的整型是沒有限制的
//但是列表的長度和容量都是由一個有具體類型的變量維護的,所以其個數肯定是有范圍的
//所以你輸入一個lst[2 ** 100000]顯然是不行的, 在Python中會報錯IndexError: cannot fit 'int' into an index-sized integer
i = PyNumber_AsSsize_t(item, PyExc_IndexError);
//設置異常
if (i == -1 && PyErr_Occurred())
return NULL;
//如果i小於0, 那么加上列表的長度, 變成正數索引
if (i < 0)
i += PyList_GET_SIZE(self);
//然后調用list_item
return list_item(self, i);
}
else if (PySlice_Check(item)) {
//......
}
else {
//......
}
}
static PyObject *
list_item(PyListObject *a, Py_ssize_t i)
{
//檢測索引i的合法性,如果i > 列表的長度, 那么會報出索引越界的錯誤。
if (!valid_index(i, Py_SIZE(a))) {
//如果索引為負數也會報出索引越界錯誤,因為上面已經對負數索引做了處理了,但如果負數索引加上長度之后還是個負數, 那么同樣報錯。
//假設列表長度是5, 你的索引是-100, 加上長度之后是-95,結果還是個負數, 所以也會報錯
if (indexerr == NULL) {
indexerr = PyUnicode_FromString(
"list index out of range");
if (indexerr == NULL)
return NULL;
}
PyErr_SetObject(PyExc_IndexError, indexerr);
return NULL;
}
//通過ob_item獲取第i個元素
Py_INCREF(a->ob_item[i]);
//返回
return a->ob_item[i];
}
顯然獲取元素的時候不光可以通過索引,還可以通過切片的方式。
static PyObject *
list_subscript(PyListObject* self, PyObject* item)
{
if (PyIndex_Check(item)) {
//.......
}
else if (PySlice_Check(item)) {
/*
start: 切片的起始位置
end: 切片的結束位置
step: 切片的步長
slicelength: 獲取元素個數,比如[1:5:2],顯然slicelength就是2, 因為只能獲取索引為1和3的元素
cur: 底層數組中元素的索引
i: 循環變量, 因為切片的話只能循環獲取每一個元素, 比如[1:5:2], 需要循環兩次。第一次循環, 上面的cur就是1, 第二次循環cur就是3
*/
Py_ssize_t start, stop, step, slicelength, cur, i;
//返回的結果
PyObject* result;
//下面代碼中會有所體現
PyObject* it;
PyObject **src, **dest;
//對切片item進行解包進行解包, 得到起始位置、結束位置、步長
if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
return NULL;
}
//計算出slicelength, 因為即便我們指定的切片是[1:3:5], 但如果列表只有3個元素, 所以slicelength也只能是1
slicelength = PySlice_AdjustIndices(Py_SIZE(self), &start, &stop,
step);
//如果slicelength為0, 那么不好意思, 表示沒有元素可以獲取, 因此直接返回一個空列表即可
if (slicelength <= 0) {
//PyList_New表示創建一個PyListObject, 里面的參數表示底層數組的長度
//另外對於創建列表,Python底層只提供了PyList_New這一種Python/C API
//當我們執行lst = [1, 2, 3]的時候就會執行PyList_New(3)
return PyList_New(0);
}
//如果步長為1, 那么會調用list_slice,這個函數內部的邏輯很簡單,首先接收一個PyListObject *和兩個整型(ilow, ihigh)
//然后在內部會創建一個PyListObject *np, 申請相應的底層數組,設置allocated
//然后將參數列表中索引為ilow的元素到索引為ihigh的元素依次拷貝到np -> ob_item里面, 然后這是ob_size並返回
else if (step == 1) {
return list_slice(self, start, stop);
}
else {
//走到這里說明步長不為1, 我們說result是一個PyListObject *, 底層數組沒有存儲在PyListObject中,而是通過ob_item發生關聯
//所以這一步是申請底層數組、設置容量的,容量就是這里的slicelength, 上面的list_slice中也調用了這一步
result = list_new_prealloc(slicelength);
if (!result) return NULL;
//src是一個二級指針, 也就是self -> ob_item
src = self->ob_item;
//同理dest是result -> ob_item
dest = ((PyListObject *)result)->ob_item;
//進行循環, cur從start開始遍歷, 每次加上step步長
for (cur = start, i = 0; i < slicelength;
cur += (size_t)step, i++) {
//it就是self -> ob_item中的元素
it = src[cur];
//增加指向的對象的引用計數
Py_INCREF(it);
//將其設置到dest中
dest[i] = it;
}
//將大小設置為slicelength,說明通過切片創建新列表, 其長度和容量也是一致的
Py_SIZE(result) = slicelength;
//返回結果
return result;
}
}
else {
//此時說明item不合法
PyErr_Format(PyExc_TypeError,
"list indices must be integers or slices, not %.200s",
item->ob_type->tp_name);
return NULL;
}
}
我們發現這個和字符串類似啊,因為通過字符串也支持切片的方式獲取。
隨着源碼的分析,我們也漸漸明朗Python的操作在底層是如何實現的了,真的一點不神秘,實現的邏輯非常簡單。
設置元素
獲取元素知道了,設置元素也不難了。
static int
list_ass_subscript(PyListObject* self, PyObject* item, PyObject* value)
{ //在list_subscript的基礎上多了一個value參數
if (PyIndex_Check(item)) {
//依舊是進行檢測i是否合法
Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);
if (i == -1 && PyErr_Occurred())
return -1;
//索引小於0,則加上列表的長度
if (i < 0)
i += PyList_GET_SIZE(self);
//調用list_ass_item進行設置,我們之前見到了list_item,是用來基於索引獲取的
//這里的list_ass_item是基於索引進行元素設置的
return list_ass_item(self, i, value);
}
else if (PySlice_Check(item)) {
//......
}
}
static int
list_ass_item(PyListObject *a, Py_ssize_t i, PyObject *v)
{
//判斷索引是否越界
if (!valid_index(i, Py_SIZE(a))) {
PyErr_SetString(PyExc_IndexError,
"list assignment index out of range");
return -1;
}
//這里的list_ass_slice后面會說
if (v == NULL)
return list_ass_slice(a, i, i+1, v);
//增加v指向對象的引用計數,因為指向它的指針被傳到了列表中
Py_INCREF(v);
//將第i個元素設置成v
Py_SETREF(a->ob_item[i], v);
return 0;
}
通過索引設置元素,邏輯很容易,關鍵是通過切片設置元素會比較復雜。而復雜的原因就在於步長,我們通過Python來演示一下。
lst = [1, 2, 3, 4, 5, 6, 7, 8]
# 首先通過切片進行設置的話, 右值一定要是一個可迭代對象
lst[0: 3] = [11, 22, 33]
# 會將lst[0]設置為11, lst[1]設置為22, lst[2]設置為33
print(lst) # [11, 22, 33, 4, 5, 6, 7, 8]
# 而且它們的長度是可以不相等的
# 這里表示將[0: 3]的元素設置為[1, 2], lst[0]設置成1, lst[1]設置成2
# 問題來了, lst[2]咋辦? 由於右值中已經沒有元素與之匹配了, 那么lst[2]就會被刪掉
lst[0: 3] = [1, 2]
print(lst) # [1, 2, 4, 5, 6, 7, 8]
# 所以我們如果想刪除[0: 3]的元素, 那么只需要執行lst[0: 3] = []即可
# 因為[]里面沒有元素能與之匹配, 所以lst中[0: 3]的元素由於匹配不到, 所以直接就沒了
# 當然由於Python的動態特性, lst[0: 3] = []、lst[0: 3] = ()、lst[0: 3] = ""等等都是可以的
lst[0: 3] = ""
print(lst) # [5, 6, 7, 8]
# 實際上我們del lst[0]的時候, 實際上就是執行了lst[0: 1] = []
# 當然如果右值元素多的話也是可以的
lst[0: 1] = [1, 2, 3, 4]
print(lst) # [1, 2, 3, 4, 6, 7, 8]
# lst[0]匹配1很好理解, 但是此時左邊已經結束了, 所以剩余的元素會依次插在后面
# 然后重點來了, 如果切片有步長的話, 那么兩邊一定要匹配
# 由於此時lst中有8個元素, lst[:: 2]會得到4個元素, 那么右邊的可迭代對象的長度也是4
lst[:: 2] = ['a', 'b', 'c', 'd']
print(lst) # ['a', 2, 'b', 4, 'c', 7, 'd']
# 但是,如果長度不一致
try:
lst[:: 2] = ['a', 'b', 'c']
except Exception as e:
# 顯然會報錯
print(e) # attempt to assign sequence of size 3 to extended slice of size 4
至於通過切片來設置元素,源碼很長,這里就不分析了,總之核心如下:
如果步長為1: 那么會調用list_ass_slice。我們說: list_item是基於索引獲取元素、list_slice是基於切片獲取元素、list_ass_item是基於索引設置元素、list_ass_slice是基於切片設置元素。而list_ass_slice內部的代碼邏輯也很長,但是核心並不難, 我們通過lst[a: b] = [v1, v2, v3, ...]這種方式就會走這里的list_ass_slice。
如果步長不為1,那么就是采用循環的方式逐個設置。
主要是考慮的情況比較多,但是核心邏輯並不復雜,有興趣可以自己去深入了解一下。
insert插入元素
insert用來在指定的位置插入元素,我們知道它是一個時間復雜度為O(n)的一個操作,因為插入位置后面的所有元素都要向后移動。
int
PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem)
{
//類型檢查
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
//底層又調用ins1
return ins1((PyListObject *)op, where, newitem);
}
static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
/*參數self:PyListObject *
參數where:索引
參數v:插入的值,這是一個PyObject *指針,因為list里面存的都是指針
*/
//i:后面for循環遍歷用的,n則是當前列表的元素個數
Py_ssize_t i, n = Py_SIZE(self);
//指向指針數組的二級指針
PyObject **items;
//如果v是NULL,錯誤的內部調用
if (v == NULL) {
PyErr_BadInternalCall();
return -1;
}
//列表的元素個數不可能無限增大,一般當你還沒創建到PY_SSIZE_T_MAX個對象時
//你內存就已經玩完了,但是python仍然做了檢測,當達到這個PY_SSIZE_T_MAX時,會報出內存溢出錯誤
if (n == PY_SSIZE_T_MAX) {
PyErr_SetString(PyExc_OverflowError,
"cannot add more objects to list");
return -1;
}
//調整列表容量,既然要inert,那么就勢必要多出一個元素
//這個元素還沒有設置進去,但是先把這個坑給留出來
//當然如果容量夠的話,是不會擴容的,只有當容量不夠的時候才會擴容
if (list_resize(self, n+1) < 0)
return -1;
//確定插入點
if (where < 0) {
//這里可以看到如果where小於0,那么我們就加上n,也就是當前列表的元素個數
//比如有6個元素,那么我們where=-1,加上6,就是5,顯然就是insert在最后一個索引的位置上
where += n;
//如果吃撐了,寫個-100,加上元素的個數還是小於0
if (where < 0)
//那么where=0,就在開頭插入
where = 0;
}
//如果where > n,那么就索引為n的位置插入,
//可元素個數為n,最大索引是n-1啊,對,所以此時就相當於append
if (where > n)
where = n;
//拿到原來的二級指針,指向一個指針數組
items = self->ob_item;
//然后不斷遍歷,把索引為i的值賦值給索引為i+1
//既然是在where處插入那么where之前的就不需要動了,到where處就停止了
for (i = n; --i >= where; )
items[i+1] = items[i];
//增加v指向的對象的引用計數,因為列表中的元素也引用了該對象
Py_INCREF(v);
//將索引為where的值設置成v
items[where] = v;
return 0;
}
所以可以看到,Python插入數據是非常靈活的。不管你在什么位置插入,都是合法的。因為它會自己調整位置,在確定位置之后,會將當前位置以及之后的所有元素向后挪動一個位置,空出來的地方設置為插入的值。
pop彈出元素
pop默認是從尾部彈出元素的,因為如果不指定索引的話,默認是-1。當然我們也可以指定索引,彈出指定索引對應的元素。
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
{
//彈出的對象的指針,因為彈出一個元素實際上是先用某個變量保存起來,然后再從列表中刪掉
PyObject *v;
//下面代碼中體現
int status;
//如果列表長度為0,顯然沒有元素可以彈, 因此會報錯
if (Py_SIZE(self) == 0) {
PyErr_SetString(PyExc_IndexError, "pop from empty list");
return NULL;
}
//索引小於0,那么加上列表的長度得到正數索引
if (index < 0)
index += Py_SIZE(self);
//依舊是調用valid_index,判斷是否越界。顯然pop沒有insert那么智能
//insert的話,索引在加上列表長度之和還小於0,那么默認是在索引為0的地方插入
//但是pop就不行了,pop的話會報出索引越界錯誤,同理索引大於等於列表長度,insert會等價於append,而pop同樣報出索引越界錯誤
if (!valid_index(index, Py_SIZE(self))) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
//根據索引獲取指定位置的元素
v = self->ob_item[index];
//這里同樣是一個快分支,如果index是最后一個元素
if (index == Py_SIZE(self) - 1) {
//那么直接調用list_resize即可,我們說只要涉及元素的添加、刪除都要執行list_resize
//至於容量是否變化,就看是否滿足newsize和allocated之間的關系,如果allocated//2 <= newsize <= allocated,那么容量就不變
//list_resize中會將ob_size設置成newsize,也就是原來的ob_size減去1, 因為是在尾部刪除的,所以只需要將ob_size設置為ob_size-1即可
status = list_resize(self, Py_SIZE(self) - 1);
//list_resize執行成功會返回0
if (status >= 0)
//直接將對象的指針返回
return v;
else
return NULL;
}
//否則說明不是快分支
Py_INCREF(v);
//這里調用了list_ass_slice, 這一步等價於self[index: index + 1] = []
status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
if (status < 0) {
//設置失敗,減少引用計數
Py_DECREF(v);
return NULL;
}
//返回指針
return v;
}
所以pop本質上也是調用了list_ass_slice。
index查詢元素的索引
index可以接收一個元素,返回該元素首次出現的索引。當然還可以額外指定一個start和end,表示查詢的范圍
static PyObject *
list_index_impl(PyListObject *self, PyObject *value, Py_ssize_t start,
Py_ssize_t stop)
{
Py_ssize_t i;
//如果start小於0,加上長度。
//還小於0,那么等於0
if (start < 0) {
start += Py_SIZE(self);
if (start < 0)
start = 0;
}
if (stop < 0) {
//如果stop小於0,加上長度
//還小於0,那么等於0
stop += Py_SIZE(self);
if (stop < 0)
stop = 0;
}
//從start開始循環
for (i = start; i < stop && i < Py_SIZE(self); i++) {
//獲取相應元素
PyObject *obj = self->ob_item[i];
//增加引用計數,因為有指針指向它
Py_INCREF(obj);
//進行比較PyObject_RichCompareBool是一個富比較,接收三個參數:元素1、元素2、操作(這里顯然是Py_EQ)
//相等返回1,不相等返回0
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
//比較完之后,減少引用計數
Py_DECREF(obj);
if (cmp > 0)
//如果相等,返回其索引
return PyLong_FromSsize_t(i);
else if (cmp < 0)
return NULL;
}
//循環走完一圈,發現都沒有相等的,那么報錯,提示元素不再列表中
PyErr_Format(PyExc_ValueError, "%R is not in list", value);
return NULL;
}
所以lst.index是一個時間復雜度為O(n)的操作,因為它在底層要循環整個列表,如果運氣好,可以第一個元素就是,運氣不好可能就好循環整個列表了。同理后面要說的if value in lst這種方式也是一樣的,因為都要循環整個列表,只不過后者返回的是一個布爾值。
count查詢元素出現的次數
static PyObject *
list_count(PyListObject *self, PyObject *value)
{
//初始為0
Py_ssize_t count = 0;
Py_ssize_t i;
//遍歷每一個元素
for (i = 0; i < Py_SIZE(self); i++) {
//獲取元素
PyObject *obj = self->ob_item[i];
//如果相等,那么count自增1,繼續下一次循環
//注意這里的相等,判斷的是什么呢?顯然是對象的地址,如果地址一樣,那么肯定指向同一個對象,所以一定相等。
if (obj == value) {
count++;
continue;
}
Py_INCREF(obj);
//走到這里說明地址不一樣,但是地址不一樣只能說明a is b不成立,但並不代表a == b不成立,所以調用PyObject_RichCompareBool進行判斷
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
Py_DECREF(obj);
//大於0,說明相等,count++
if (cmp > 0)
count++;
else if (cmp < 0)
return NULL;
}
//返回count
return PyLong_FromSsize_t(count);
}
count毫無疑問,無論在什么情況下,它都是一個時間復雜度為O(n)的操作,因為列表必須要從頭遍歷到尾。
remove根據元素的值刪除元素
除了根據索引刪除元素之外,也可以元素指向的對象維護的值刪除元素,刪除第一個出現元素。
static PyObject *
list_remove(PyListObject *self, PyObject *value)
{
Py_ssize_t i;
for (i = 0; i < Py_SIZE(self); i++) {
//從頭開始遍歷,獲取元素
PyObject *obj = self->ob_item[i];
Py_INCREF(obj);
//比較是否相等
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
Py_DECREF(obj);
//如果相等
if (cmp > 0) {
//調用list_ass_slice刪除元素
if (list_ass_slice(self, i, i+1,
(PyObject *)NULL) == 0)
//返回None
Py_RETURN_NONE;
return NULL;
}
else if (cmp < 0)
return NULL;
}
//否則說明元素不在列表中
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
return NULL;
}
reverse翻轉列表
static PyObject *
list_reverse_impl(PyListObject *self)
{
//如果列表長度不大於1的話, 那么直接返回其本身即可
if (Py_SIZE(self) > 1)
//大於1的話,執行reverse_slice, 傳遞了兩個參數
//第一個參數self -> ob_item顯然是底層數組首元素的地址
//而第二個參數self->ob_item + Py_SIZE(self)則是底層數組中索引為ob_size的元素的地址
//但是很明顯能訪問的最大索引應該是ob_size - 1才對, 別急我們繼續往下看, 看一下reverse_slice函數的實現
reverse_slice(self->ob_item, self->ob_item + Py_SIZE(self));
Py_RETURN_NONE;
}
static void
reverse_slice(PyObject **lo, PyObject **hi)
{
assert(lo && hi);
//我們看到又執行了一次--hi,將hi移動到了ob_size - 1位置,也就是說此時二級指針hi保存的還是索引為ob_size - 1的元素的值
//所以個人覺得有點納悶, 直接reverse_slice(self->ob_item, self->ob_item + Py_SIZE(self) - 1);不行嗎
--hi;
//當lo小於hi的時候
while (lo < hi) {
PyObject *t = *lo;
*lo = *hi;
*hi = t;
//上面三步就等價於 *lo, *hi = *hi, *lo, 但是C不支持這么寫
//所以我們看到就是將索引為0的元素和索引為ob_size-1的元素進行了交換,前后兩個指針繼續靠近,指向的元素繼續交換,知道兩個指針相遇
++lo;
--hi;
}
}
所以到現在,你還認為Python中的列表神秘嗎?雖然我們不可能寫出一個Python解釋器,但是底層的一些思想其實並沒有那么難,作為一名程序猿很容易想的到。
兩個列表相加
static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
Py_ssize_t size; //相加之后的列表長度
Py_ssize_t i; //循環變量
//兩個二級指針,指向ob_item
PyObject **src, **dest;
//新的列表
PyListObject *np;
//類型檢測
if (!PyList_Check(bb)) {
PyErr_Format(PyExc_TypeError,
"can only concatenate list (not \"%.200s\") to list",
bb->ob_type->tp_name);
return NULL;
}
#define b ((PyListObject *)bb)
//判斷長度是否溢出
if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b))
return PyErr_NoMemory();
//計算新列表的長度
size = Py_SIZE(a) + Py_SIZE(b);
//設置np -> ob_item指向的底層數組
np = (PyListObject *) list_new_prealloc(size);
if (np == NULL) {
return NULL;
}
//獲取a -> ob_item和np -> ob_item
src = a->ob_item;
dest = np->ob_item;
//將元素依次拷貝過去, 增加引用計數
for (i = 0; i < Py_SIZE(a); i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
//獲取b->ob_item
//獲取np->ob_item + Py_SIZE(a), 要從Py_SIZE(a)的位置開始設置, 否則就把之前的元素覆蓋掉了
src = b->ob_item;
dest = np->ob_item + Py_SIZE(a);
//將元素依次拷貝過去, 增加引用計數
for (i = 0; i < Py_SIZE(b); i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
//設置ob_size
Py_SIZE(np) = size;
//返回np
return (PyObject *)np;
#undef b
}
判斷元素是否在列表中
對於一個序列來說,可以使用in操作符,等價於調用其__contains__
魔法方法。
static int
list_contains(PyListObject *a, PyObject *el)
{
PyObject *item;
Py_ssize_t i;
int cmp;
//挨個循環,比較是否相等。存在cmp會等於1,cmp == 0 && i < Py_SIZE(a)不滿足,直接返回
//不相等則為0, 會一直比完列表中所有的元素
for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i) {
item = PyList_GET_ITEM(a, i);
Py_INCREF(item);
cmp = PyObject_RichCompareBool(el, item, Py_EQ);
Py_DECREF(item);
}
return cmp;
}
真的非常簡單,沒有什么好說的。
列表的深淺拷貝
列表的深淺拷貝也是初學者容易犯的錯誤之一,我們看一個Python的例子。
lst = [[]]
# 默認是淺拷貝, 這個過程會創建一個新列表, 會將里面的指針拷貝一份
# 但是指針指向的內存並沒有拷貝
lst_cp = lst.copy()
# 兩個對象的地址是一樣的
print(id(lst[0]), id(lst_cp[0])) # 2207105155392 2207105155392
# 操作lst[0], 會改變lst_cp[0]
lst[0].append(123)
print(lst, lst_cp) # [[123]] [[123]]
# 操作lst_cp[0], 會改變lst[0]
lst_cp[0].append(456)
print(lst, lst_cp) # [[123, 456]] [[123, 456]]
我們通過索引或者切片也是一樣的道理
lst = [[], 1, 2, 3]
val = lst[0]
lst_cp = lst[0: 1]
print(lst[0] is val is lst_cp[0]) # True
# 此外,lst[:]完全等價於lst.copy()
之所以會有這樣現象,是因為我們說過Python中變量、容器里面的元素都是一個泛型指針PyObject *,在傳遞的時候會傳遞指針, 但是在操作的時候會操作指針指向的內存。
所以lst.copy()就是創建了一個新列表,然后把元素拷貝了過去,並且這里的元素是指針。因為只是拷貝指針,沒有拷貝指針指向的對象
(內存)
,所以它們的地址都是一樣的,因為指向的是同一個對象。
但如果我們就想在拷貝指針的同時也拷貝指針指向的對象呢?答案是使用一個叫copy的模塊。
import copy
lst = [[]]
# 此時拷貝的時候,就會把指針指向的對象也給拷貝一份
lst_cp1 = copy.deepcopy(lst)
lst_cp2 = lst[:]
lst_cp2[0].append(123)
print(lst) # [[123]]
print(lst_cp1) # [[]]
# lst[:]這種方式也是淺拷貝, 所以修改lst_cp2[0], 也會影響lst[0]
# 但是沒有影響lst_cp1[0], 證明它們是相互獨立的, 因為指向的是不同的對象
淺拷貝示意圖如下:
里面的兩個底層數組的元素是一樣的
深拷貝示意圖如下:
里面的兩個底層數組的元素是不一樣的
注意:copy.deepcopy雖然在拷貝指針的同時會將指針指向的對象也拷貝一份,但這僅僅是針對於可變對象,對於不可變對象是不會拷貝的。
import copy
lst = [[], "古明地覺"]
lst_cp = copy.deepcopy(lst)
print(lst[0] is lst_cp[0]) # False
print(lst[1] is lst_cp[1]) # True
為什么會這樣,其實原因很簡單。因為不可變對象是不支持本地修改的,你若想修改只能指向新的對象,但是對其它的變量則沒有影響,其它變量該指向誰就還指向誰。因為a = b只是將b的指針拷貝一份給a,然后a和b都指向了同一個對象,至於a和b本身則是沒有任何關系的。如果此時b指向了新的對象,是完全不會影響a的,a還是指向原來的對象。所以如果一個指針指向的對象不支持本地修改,那么深拷貝不會拷貝對象本身,因為指向的是不可變對象,所以不會有修改一個影響另一個的情況出現。
關於列表還有一些陷阱:
lst = [[]] * 5
lst[0].append(1)
print(lst) # [[1], [1], [1], [1], [1]]
# 列表乘上一個n,等於把列表里面的元素重復n次
# 注意: 類似於lst = [1, 2, 3], 雖然我們寫的是整數,但是它存儲的並不是整數,而是其指針
# 所以會把指針重復5次, 因此列表里面5個指針都指向了同一個列表
# 這種方式創建的話,里面的元素都指向了不同的列表
lst = [[], [], [], [], []]
lst[0].append(1)
print(lst) # [[1], [], [], [], []]
# 再比如字典,在后續系列中會說
d = dict.fromkeys([1, 2, 3, 4], [])
print(d) # {1: [], 2: [], 3: [], 4: []}
d[1].append(123)
print(d) # {1: [123], 2: [123], 3: [123], 4: [123]}
# 它們都指向了同一個列表,因此這種陷阱在工作中要注意, 因為一不小心就會出現大問題
創建PyListObject
我們說創建一個列表,Python底層只提供了唯一一個Python/C API,也就是PyList_New。這個函數接收一個size參數,從而允許我們在創建一個PyListObject對象時指定底層數組的長度。
PyObject *
PyList_New(Py_ssize_t size)
{
//聲明一個PyListObject *對象
PyListObject *op;
#ifdef SHOW_ALLOC_COUNT
static int initialized = 0;
if (!initialized) {
Py_AtExit(show_alloc);
initialized = 1;
}
#endif
//如果size小於0,直接拋異常
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
//緩存池是否可用,如果可用
if (numfree) {
//將緩存池內對象個數減1
numfree--;
//從緩存池中獲取
op = free_list[numfree];
//設置引用計數
_Py_NewReference((PyObject *)op);
#ifdef SHOW_ALLOC_COUNT
count_reuse++;
#endif
} else {
//不可用的時候,申請內存
op = PyObject_GC_New(PyListObject, &PyList_Type);
if (op == NULL)
return NULL;
#ifdef SHOW_ALLOC_COUNT
count_alloc++;
#endif
}
//如果size小於等於0,ob_item設置為NULL
if (size <= 0)
op->ob_item = NULL;
else {
//否則的話,創建一個指定容量的指針數組,然后讓ob_item指向它
//所以是先創建PyListObject對象, 然后創建底層數組, 最后通過ob_item建立聯系
op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}
}
//設置ob_size和allocated,然后返回op
Py_SIZE(op) = size;
op->allocated = size;
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
我們注意到源碼里面有一個緩沖池,是的,創建PyListObject對象時,會先檢測緩沖池free_lists里面是否有可用的對象,有的話直接拿來用,否則通過malloc在系統堆上申請。緩沖池中最多維護80個PyListObject對象。
/* Empty list reuse scheme to save calls to malloc and free */
#ifndef PyList_MAXFREELIST
#define PyList_MAXFREELIST 80
#endif
static PyListObject *free_list[PyList_MAXFREELIST];
根據之前的經驗我們知道,既然能從緩存池中獲取,那么在執行析構函數的時候也要把列表放到緩存池里面。
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);
//將底層數組中每個指針指向的對象的引用計數都減去1
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]);
}
//然后釋放底層數組所占的內存
PyMem_FREE(op->ob_item);
}
//判斷緩沖池里面PyListObject對象的個數,如果沒滿,就添加到緩存池
//注意:我們看到執行到這一步的時候, 底層數組已經被釋放掉了
if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
free_list[numfree++] = op;
else
//否則的話再釋放掉PyListObject對象所占的內存
Py_TYPE(op)->tp_free((PyObject *)op);
Py_TRASHCAN_SAFE_END(op)
}
我們知道在創建一個新的PyListObject對象時,實際上是分為兩步的,先創建PyListObject對象,然后創建底層數組,最后讓PyListObject對象中的ob_item成員指向這個底層數組。同理,在銷毀一個PyListObject對象時,先銷毀ob_item維護的底層數組,然后再釋放PyListObject對象自身(如果緩存池已滿的情況下)
。
現在可以很清晰地明白了,原本空盪盪的緩存池其實是被已經死去的PyListObject對象填充了,在以后創建新的PyListObject對象時,Python會首先喚醒這些死去的PyListObject對象,給它們一個洗心革面、重新做人的機會。但需要注意,這里緩存的僅僅是PyListObject對象,對於底層數組,其ob_item已經不再指向了。從list_dealloc中我們看到,PyListObject對象在放進緩存池之前,ob_item指向的數組就已經被釋放掉了,同時數組中指針指向的對象的引用計數會減1。所以最終數組中這些指針指向的對象也大難臨頭各自飛了,或生存、或毀滅,總之此時和PyListObject之間已經沒有任何聯系了。但是為什么要這么做呢?為什么不連底層數組也一起維護呢?可以想一下,如果繼續維護的話,數組中指針指向的對象永遠不會被釋放,那么很可能會產生懸空指針的問題,所以這些指針指向的對象所占的空間必須交還給系統(前提是沒有其它指針指向了)
。
但是實際上,是可以將PyListObject對象維護的底層數組進行保留的,即只將數組中指針指向的對象的引用計數減1,然后將數組中的指針都設置為NULL,不再指向之前的對象了,但是並不釋放底層數組本身所占用的內存空間。因此這樣一來,釋放的內存不會交給系統堆,那么再次分配的時候,速度會快很多。但是這樣帶來一個問題,就是這些內存沒人用也會一直占着,並且只能供PyListObject對象的ob_item指向的底層數組使用,因此Python還是為避免消耗過多內存,采取將底層數組的內存交換給了系統堆這樣的做法,在時間和空間上選擇了空間。
元組的底層結構--PyTupleObject
因為元組比較簡單,和列表比較相似,所以就放在一起介紹了。我們知道元組,就相當於不支持元素添加、修改、刪除等操作的列表。
元組的實現機制非常簡單,可以看做是在列表的基礎上刪除了增刪改
等操作。既然如此,那要元組有什么用呢?畢竟元組的功能只是列表的子集。元組存在的最大一個特點就是,它可以作為字典的key、以及可以作為集合的元素。因為字典和集合存儲數據的原理是哈希表,字典和集合我們后續章節會說。對於列表這樣的可變對象來說是可以動態改變的,而哈希值是一開始就計算好的,顯然如果支持動態修改的話,那么哈希值肯定會變,這是不允許的。所以如果我們希望字典的key是一個序列,顯然元組再適合不過了。
從tuple的特點也能看出:tuple的底層是一個變長對象,但同時也是一個不可變對象。
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
可以看到,對於不可變對象來說,它底層結構體定義也非常簡單。一個引用計數、一個類型、一個指針數組。這里的1可以想象成n,我們在PyLongObject中說過。
並且我們發現不像列表,元組沒有allocated,這是因為它是不可變的,不支持resize操作。至於維護的值,同樣是指針組成的數組,數組里面的每一個指針都指向了具體的值。
PyTupleObject的創建
正如列表一樣,Python創建PyTupleObject也提供了類似的初始化方法。
PyObject *
PyTuple_New(Py_ssize_t size)
{
//PyTupleObject指針
PyTupleObject *op;
Py_ssize_t i;
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
#if PyTuple_MAXSAVESIZE > 0
//元組同樣有緩存池
if (size == 0 && free_list[0]) {
op = free_list[0];
Py_INCREF(op);
#ifdef COUNT_ALLOCS
tuple_zero_allocs++;
#endif
//如果長度為0,那么直接返回
return (PyObject *) op;
}
if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
//從緩存池中獲取
free_list[size] = (PyTupleObject *) op->ob_item[0];
numfree[size]--;
#ifdef COUNT_ALLOCS
fast_tuple_allocs++;
#endif
/* Inline PyObject_InitVar */
#ifdef Py_TRACE_REFS
//設置ob_size,和ob_type
Py_SIZE(op) = size;
Py_TYPE(op) = &PyTuple_Type;
#endif
//引用計數初始化為1
_Py_NewReference((PyObject *)op);
}
else
#endif
{
/* 元組的元素個數同樣有限制,但我們說這個限制一般達不到 */
if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) -
sizeof(PyObject *)) / sizeof(PyObject *)) {
return PyErr_NoMemory();
}
//申請空間
op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
if (op == NULL)
return NULL;
}
for (i=0; i < size; i++)
op->ob_item[i] = NULL;
#if PyTuple_MAXSAVESIZE > 0
if (size == 0) {
free_list[0] = op;
++numfree[0];
Py_INCREF(op); /* extra INCREF so that this is never freed */
}
#endif
#ifdef SHOW_TRACK_COUNT
count_tracked++;
#endif
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
和PyListObject初始化類似,同樣需要做一些類型檢測,內存是否溢出等等。
當然有了列表的經驗,元組的一些底層操作我們就不分析了,它是列表的子集。
靜態資源緩存
列表和元組兩者在通過索引查找元素的時候是一致的,但是元組除了能作為字典的key之外,還有一個特點,就是分配的速度比較快。一方面是因為由於其不可變性,使得在編譯的時候就確定了,另一方面就是它還具有靜態資源緩存的作用。
對於一些靜態變量,比如元組,如果它不被使用並且占用空間不大時,Python 會暫時緩存這部分內存。這樣,下次我們再創建同樣大小的元組時,Python 就可以不用再向操作系統發出請求,去尋找內存,而是可以直接分配之前緩存的內存空間,這樣就能大大加快程序的運行速度。你可以理解為PyTupleObject對象在被析構時,不僅對象本身沒有被回收,連底層的指針數組也被緩存起來了。
from timeit import timeit
t1 = timeit(stmt="x1 = [1, 2, 3, 4, 5]", number=1000000)
t2 = timeit(stmt="x2 = (1, 2, 3, 4, 5)", number=1000000)
print(round(t1, 2)) # 0.05
print(round(t2, 2)) # 0.01
可以看到用時,元組只是列表的五分之一。這便是元組的另一個優勢,可以將資源緩存起來。
小結
這次我們介紹了Python中列表的底層實現,以及它的相關操作。當然內容都在正文里面,也沒啥好總結的了。話說寫到這里,我的typora明顯變卡了。。。。。。