老Python帶你從淺入深探究List

雲崖先生發表於2021-05-13

列表

Python中的列表(list)是最常用的資料型別之一。

Python中的列表可以儲存任意型別的資料,這與其他語言中的陣列(array)不同。

被存入列表中的內容可稱之為元素(element)或者資料項(data item)亦或是值(value)。

雖然Python列表支援儲存任意型別的資料項,但不建議這麼做,事實上這麼做的概率也很低。

列表特性

列表的特點:

  1. 列表屬於容器序列
  2. 列表屬於可變型別,即物件本身的屬性會根據外部變化而變化,例如長度
  3. 列表底層由順序儲存組成,而順序儲存是線性結構的一種

基本宣告

以下是使用類例項化的形式進行物件宣告:

li = list((1, 2, 3, 4, 5))
print("值:%r,型別:%r" % (li, type(li)))

# 值:[1, 2, 3, 4, 5],型別:<class 'list'>

也可以選擇使用更方便的字面量形式進行物件宣告,利用[]對資料項進行包裹,並且使用逗號將資料項之間進行分割:

li = [1, 2, 3, 4, 5]
print("值:%r,型別:%r" % (li, type(li)))

# 值:[1, 2, 3, 4, 5],型別:<class 'list'>

多維列表

當一個列表中巢狀另一個列表,該列表就可以稱為多維列表。

如下,定義一個2維列表:

li = [1, 2, ["三","四"]]
print("值:%r,型別:%r" % (li, type(li)))

值:[1, 2, ['三', '四']],型別:<class 'list'>

續行操作

在Python中,列表中的資料項如果過多,可能會導致整個列表太長,太長的列表是不符合PEP8規範的。

  • 每行最大的字元數不可超過79,文件字元或者註釋每行不可超過72

Python雖然提供了續行符\,但是在列表中可以忽略續行符,如下所示:

li = [
    1,
    2,
    3,
    4,
    5
]
print("值:%r,型別:%r" % (li, type(li)))

# 值:[1, 2, 3, 4, 5],型別:<class 'list'>

型別轉換

列表支援與布林型、字串、元組、以及集合型別進行型別轉換:

li = [1, 2, 3]
bLi = bool(li)    # 布林型別
strLi = str(li)   # 字串型別
tupLi = tuple(li) # 元組型別
setLi = set(li)   # 集合型別

print("值:%r,型別:%r" % (bLi, type(bLi)))
print("值:%r,型別:%r" % (strLi, type(strLi)))
print("值:%r,型別:%r" % (tupLi, type(tupLi)))
print("值:%r,型別:%r" % (setLi, type(setLi)))

# 值:True,型別:<class 'bool'>
# 值:'[1, 2, 3]',型別:<class 'str'>
# 值:(1, 2, 3),型別:<class 'tuple'>
# 值:{1, 2, 3},型別:<class 'set'>

如果一個2維列表遵循一定的規律,那麼也可以將其轉換為字典型別:

li = [["k1", "v1"], ["k2", "v2"], ["k3", "v3"]]
dictList = dict(li)

print("值:%r,型別:%r" % (dictList, type(dictList)))

# 值:{'k1': 'v1', 'k2': 'v2', 'k3': 'v3'},型別:<class 'dict'>

四則運算

列表支援與元組,列表進行加法運算:

  • +:合併2個列表並生成新列表:

    li1 = [1, 2, 3]
    li2 = [4, 5, 6]
    newLi = li1 + li2
    print(newLi)
    
    # [1, 2, 3, 4, 5, 6]
    
  • +=:擴充套件已有列表,相當於extend()方法:

    oldLi = [1, 2, 3]
    newLi = [4, 5, 6]
    oldLi += newLi
    print(oldLi)
    
    # [1, 2, 3, 4, 5, 6]
    

列表支援與數字進行乘法運算:

  • *:生成一個重複舊列表資料項的新列表:

    oldLi = [1, 2, 3]
    newLi = oldLi * 3
    print(newLi)
    
    # [1, 2, 3, 1, 2, 3, 1, 2, 3]
    
  • *=:擴充套件已有列表,將已有列表的資料項進行重複新增:

    oldLi = [1, 2, 3]
    oldLi *= 3
    print(oldLi)
    
    # [1, 2, 3, 1, 2, 3, 1, 2, 3]
    

索引切片

索引的概念

列表底層是以一種連續的順序結構儲存資料項,故可以使用索引(index)對資料項進行獲取、刪除、擷取、替換等操作。

----------------------------|
| A | B | C | D | E | F | G |
----------------------------|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|-7 |-6 |-5 |-4 |-3 |-2 |-1 |

正向索引都是從0開始,負向索引都是從-1開始。

enumerate()

我們以一個內建函式enumerate()來舉例,該函式返回一個迭代器,將其轉換為list()後可以檢視資料項以及正向索引:

li = ["A", "B", "C", "D", "E", "F", "G"]
print(list(enumerate(li)))

# [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D'), (4, 'E'), (5, 'F'), (6, 'G')]

更多的關於enumerate()函式的妙用,將會放在迴圈章節中進行探討。

slice()

slice()函式有三個引數:

  • start:索引開始的位置

  • stop:索引結束的位置

  • step:步長間距,預設為1

    如果為正數代表正取,如果為負數代表倒取

    如果為1代表連續取,如果為2代表隔一個取一個,以此類推

使用方法如下示例,先使用slice()確定擷取資料項的範圍,然後再使用列表的[]操作形式取出指定範圍的資料項。

注意:所有的切片都是顧頭不顧尾:

li = ["A", "B", "C", "D", "E", "F", "G"]

# 取出A
# 釋義:從0開始取,取1個,步長為0
s = slice(0, 1, None)
print(li[s])

# ['A']

[::]形式

使用slice()函式會比較繁瑣,直接使用[::]形式進行切片會比較簡單。

簽名如下:

[start:stop:step]

引數描述和slice()相同,當某一引數不設定時可省略前面的引數。

增刪改截操作演示:

>>> # 獲取第2個資料項
>>> li = ["A", "B", "C", "D", "E", "F", "G"]
>>> li[1]
'B'
>>> # 刪除第3個資料項
>>> del li[2]
>>> li
['A', 'B', 'D', 'E', 'F', 'G']
>>> # 從第1個資料項開始向後替換2個資料項,替換內容為123
>>> li[0:3] = 1,2,3
>>> li
[1, 2, 3, 'E', 'F', 'G']
>>> # 從第1個資料項開始獲取2個資料項
>>> li[0:3]
[1, 2, 3]
>>> # 試圖訪問一個超出索引之外的資料項,將引發異常
>>> li[100]
IndexError: list index out of range

需要注意的是,如果使用[:]則會建立一個新的列表,再將原有列表中的資料項全部引用至新的列表中,類似於淺拷貝的概念。

如下所示:

l1 = ["A", "B", "C", "D", "E", "F", "G"]
l2 = l1[:]

print("值:%s,地址:%s" % (l1, id(l1)))
print("值:%s,地址:%s" % (l2, id(l2)))

# 值:['A', 'B', 'C', 'D', 'E', 'F', 'G'],地址:4366350216
# 值:['A', 'B', 'C', 'D', 'E', 'F', 'G'],地址:4366660104

正向切片

正向切片即使用正向索引,索引從0開始進行切片。

如下示例:

>>> li = ["A", "B", "C", "D", "E", "F", "G"]
>>> li[0:3]
['A', 'B', 'C']

反向切片

反向切片即使用反向索引,索引從-1開始進行切片。

如下示例:

>>> li = ["A", "B", "C", "D", "E", "F", "G"]
>>> li[-3:]
['E', 'F', 'G']

多維切片

列表是支援多維切片的,如下示例,拿出2維列表中的資料項A:

>>> li = [1, 2, ["A", "B"]]
>>> li[-1][0]
'A'

高階切片

反向和正向切片可以混合使用,下面是一些高階切片的用法:

>>> li = ["A", "B", "C", "D", "E", "F", "G"]
>>> li[:] ①
['A', 'B', 'C', 'D', 'E', 'F', 'G']
>>> li[2:4] ②
['C', 'D']
>>> li[::2] ③
['A', 'C', 'E', 'G'] 
>>> li[::-2]
['G', 'E', 'C', 'A'] ④
>>> li[::-1] ⑤
['G', 'F', 'E', 'D', 'C', 'B', 'A'] 
>>> li[:-5:-2] ⑥
['G', 'E']
>>> li[0::-1] ⑦
['A']
>>> li[5::-2] ⑧
['F', 'D', 'B']

如何一眼讀懂高階切片,通過以下步驟判定:

  • 第一步先觀察step,是負數還是正數,負數代表倒著取
  • 第二步觀察start,確定切片的開始位置
  • 第三步觀察stop,確定切片的結束位置

上述示例演示的說明,帶*的是比較重要的方式。

①:列表的[:]操作是建立一個新的列表,再將原有列表中的資料項全部引用一次至新的列表*

②:步長為空,代表正著取,從第2號索引位置的資料項開始,到第4號索引位置的資料項結束,根據顧頭不顧尾原則,取2,3號索引的資料項*

③:步長為2,代表正著取,隔一個取一個,即從0號索引位置的資料項開始,每次往後數到2的時候再取*

④:步長為-2,代表倒著取,隔一個取一個,即從-1號索引位置的資料項開始,每次向前數到2的時候再取*

⑤:步長為-1,代表倒著取,即從-1號索引位置的資料項開始,取到索引0號位置結束*

⑥:步長為-2,代表倒著取,隔一個位置取一個,取到-5索引位置結束,根據顧頭不顧尾原則,不取-5索引位置的資料項

⑦:步長為-1,代表倒著取,開始位置為0,結束位置未標記,則取1個

⑧:步長為-2,代表倒著取,隔一個取一個,開始位置為5號索引,則從第5號索引開始向前取

解構方法

*語法

使用*語法可對列表進行解構,將列表中的資料項全部提取出來:

li = [1, 2, 3]
print(*li)

# 1 2 3

我們可以利用*語法的特性,來達到兩個列表進行合併產生新列表的效果,類似於+:

l1 = [1, 2, 3]
l2 = [4, 5, 6]
result = [*l1, *l2]
print(result)

# [1, 2, 3, 4, 5, 6]

解構賦值

如果一個列表中的資料項需要賦值到變數中,可使用解構賦值,需要注意的是變數接收位置與列表中的資料項位置需要一一對應:

li = ["A", "B"]
item1, item2 = li

print(item1, item2)

# A B

我們只想取出列表中前2個資料項時,可使用*語法將剩下的資料項全部打包到一個變數中:

li = ["A", "B", "C", "D", "E", "F", "G"]
item1, item2, *otherItems = li

print(item1, item2)
print(otherItems)

# A B
# ['C', 'D', 'E', 'F', 'G']

如果只想取第1個後和最後2個,中間的都不想要怎麼辦?也可以通過*語法:

li = ["A", "B", "C", "D", "E", "F", "G"]
itemFirst, *_, itemLast1, itemLast2 = li # ①

print(itemFirst, itemLast1, itemLast2)
print(_)

# A F G
# ['B', 'C', 'D', 'E']

①:_為匿名變數,參見變數與常量一章節中的釋義

常用方法

方法一覽

常用的list方法一覽表:

方法名 返回值 描述
append() None 將資料項新增至列表的末尾
extend() None 通過附加來自可迭代物件的資料項來擴充套件列表
insert() None 在索引之前插入物件
pop() item 刪除並返回索引處的專案(預設為-1)。如果列表為空或索引超出範圍,則引發IndexError
copy() list 返回L的淺拷貝
remove() None 刪除列表中第一次出現的資料項。如果不存在該資料項,則引發ValueError
clear() None 從L移除所有專案
count() integer 返回資料項在L中出現的次數
index() integer 返回第一個資料項在L中出現位置的索引,若值不存在,則丟擲ValueError
sort() None 對列表進行原地排序,可指定引數reverse,若不指定該引數則預設升序排列,指定該引數則為降序排列

基礎公用函式:

函式名 返回值 描述
len() integer 返回容器中的專案數
enumerate() iterator for index, value of iterable 返回一個可迭代物件,其中以小元組的形式包裹資料項與正向索引的對應關係
reversed() ... 詳情參見函式章節
sorted() ... 詳情參見函式章節

獲取長度

使用len()方法來獲取列表的長度。

返回int型別的值。

li = ["A", "B", "C", "D", "E", "F", "G"]

print(len(li))

# 7

Python在對內建的資料型別使用len()方法時,實際上是會直接的從PyVarObject結構體中獲取ob_size屬性,這是一種非常高效的策略。

PyVarObject是表示記憶體中長度可變的內建物件的C語言結構體。

直接讀取這個值比呼叫一個方法要快很多。

追加元素

使用append()方法為當前列表追加一個資料項。

返回None:

li = ["A", "B", "C", "D", "E", "F", "G"]

li.append("H")

print(li)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

你也可以使用+=進行操作,但個人並不推薦這樣使用。

在某些極端情況下,可能會出現一些難以察覺的Bug:

li = ["A", "B", "C", "D", "E", "F", "G"]

li += "H"

print(li)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

列表合併

使用extend()方法來讓當前列表與另一個可迭代物件進行合併。

返回None:

l1 = ["A", "B", "C", "D", "E", "F", "G"]
l2 = ["H", "J", "K", "L"]

l1.extend(l2)

print(l1)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L']

你也可以使用+=進行操作,但個人並不推薦這樣使用。

在某些極端情況下,可能會出現一些難以察覺的Bug:

l1 = ["A", "B", "C", "D", "E", "F", "G"]
l2 = ["H", "J", "K", "L"]

l1 += l2

print(l1)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L']

插入元素

使用insert()方法將資料項插入當前列表中的指定位置。

返回None:

li = ["A", "B", "C", "D", "E", "F", "G"]

li.insert(0, "a")

print(li)

# ['a', 'A', 'B', 'C', 'D', 'E', 'F', 'G']

列表拷貝

使用copy()方法將當前列表進行淺拷貝。

返回當前列表的拷貝物件:

oldLi = ["A", "B", "C", "D", "E", "F", "G"]
newLi = oldLi.copy()

print(oldLi)
print(newLi)

print(id(oldLi))
print(id(newLi))

print(id(oldLi[0]))
print(id(oldLi[0]))

# ['A', 'B', 'C', 'D', 'E', 'F', 'G']
# ['A', 'B', 'C', 'D', 'E', 'F', 'G']
# 4329305992
# 4329607688
# 4328383520
# 4328383520

彈出元素

使用pop()方法彈出當前列表中在此索引位置的資料項,列表中該資料項將被刪除,並返回被彈出的資料項。

若不指定位置,則預設彈出-1號索引位置的資料項:

li = ["A", "B", "C", "D", "E", "F", "G"]

popItem1 = li.pop()
popItem2 = li.pop(2)

print(li)
print(popItem1)
print(popItem2)

# ['A', 'B', 'D', 'E', 'F']
# G
# C

刪除元素

使用remove()方法刪除當前列表中的資料項,根據資料項的名字進行刪除。

如果具有多個同名的資料項,則只會刪除第一個。

返回None:

li = ["A", "A", "B", "C", "D", "E", "F", "G"]

li.remove("A")

print(li)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G']

我們也可以使用del li[index]進行資料項的刪除:

li = ["A", "A", "B", "C", "D", "E", "F", "G"]

del li[0]

print(li)

# ['A', 'B', 'C', 'D', 'E', 'F', 'G']

這種方式還可以刪除多個:

li = ["A", "A", "B", "C", "D", "E", "F", "G"]

del li[0:3]

print(li)

# ['C', 'D', 'E', 'F', 'G']

清空元素

使用clear()方法將當前列表進行清空,即刪除所有資料項。

返回None:

li = ["A", "B", "C", "D", "E", "F", "G"]

li.clear()

print(li)

# []

我們也可以使用del li[:]進行列表的清空:

li = ["A", "B", "C", "D", "E", "F", "G"]

del li[:]

print(li)

# []

統計次數

使用count()方法統計資料項在該列表中出現的次數。

返回int:

li = ["A", "B", "C", "D", "E", "F", "G", "A"]

aInLiCount = li.count("A")

print(aInLiCount)

# 2

查詢位置

使用index()方法找到資料項在當前列表中首次出現的位置索引值,如資料項不存在則丟擲異常。

返回int。

li = ["A", "B", "C", "D", "E", "F", "G", "A"]

aInLiIndex = li.index("A")

print(aInLiIndex)

# 0

順序排列

使用sort(li)將當前列表中的資料項按照ASCII碼順序進行排列,預設從小到大。

可指定引數resverse,用於是否翻轉列表,如翻轉列表則代表從大到小。

返回None:

li = ["A", "B", "C", "D", "E", "F", "G", "A"]

li.sort(reverse=True)

print(li)

# 'G', 'F', 'E', 'D', 'C', 'B', 'A', 'A']

sort()方法內部採用timsort演算法,這是一種非常優秀的演算法,速度快且穩定。

底層探究

容器序列

這裡引出一個新的概念,容器序列:

  • 容器中能存放不同型別的資料項,如list就是標準的一個容器
  • 序列是指具有長度的物件,且該物件能使用[]進行內部資料項的操作

容器序列存放的是它們所包含的任意型別的物件的引用,如下定義了一個列表:

x = [“A”, “B”, “C”]

它的內部儲存結構如下圖所示:

image-20210512210028804

在CPython原始碼中,列表資料項的引用為PyObject **ob_item屬性,即指標的指標。

元素調整

列表中,pop()和insert()方法都具有指定索引值的功能。

如果使用pop(0),或者insert(0)則都會引起整個列表中資料項的調整。

  • pop(0)會將隊首的資料項彈出並刪除,後面的所有資料項都要向前挪一個位置。1變成0,2變成1,以此類推
  • insert(0)會將新的資料項插入至隊首,後面的所有資料項都要向後挪一個位置。0變成1,1變成2,以此類推

而單純的使用pop()或者append()則不會發生元素調整,因為它們總是在隊尾做操作。

總而言之,任何一個方法只要不是操作的隊尾資料項,都會引起該列表中其他所有資料項的調整。

圖示如下:

image-20210513103433645

image-20210513103539319

擴容機制

在對列表進行新增資料項時,如果列表內部的容量已滿則會觸發擴容機制。

我們要理解2個概念:

  • 容量:容量指的是列表底層在開闢記憶體時,開闢了多大的記憶體空間,能夠容納多少資料項,可以理解為一共有多少個槽位
  • 大小:大小指的是當前列表中,資料項已佔據的容量,可以理解為已用了多少個槽位

在CPython原始碼中,列表容量的屬性為Py_ssize_t allocated,而列表大小的屬性為Py_ssize_t ob_size

在初始化列表時,容量和大小總是等於資料項的總個數,如:

  • 一個空列表,容量和大小都為0
  • 有8個槽位的列表,容量和大小都為8

如果對一個已有的列表進行增加資料項的操作時會有以下2條判斷,判定當前列表是否需要擴容:

  • 當前列表容量 > 已有資料項個數+1 and 已有資料項個數 >= 當前列表容量的一半

    則直接新增資料項,不進行擴容

    並且新增資料項個數 Py_ssize_t ob_size + 1

  • 當前列表容量 < 已有資料項個數+1

    則先進行擴容後再新增資料項

    擴容是一種線性增長,增長規律為:0、4、8、16、24、32、40、52、64、76 …,總是為4的倍數

    在擴容時不必擔心發生記憶體溢位,因為內部已經設定了最大值

    為:PY_SSIZE_T_MAX *(9/8)+ 6

image-20210513104211272

縮容機制

縮容機制建立在列表有空餘空間的情況下。

我們如果使用pop()方法刪除了最後一個資料項,其實並不會將最後一個列表槽位所佔用的記憶體空間給釋放掉而是進行保留,內部僅進行一次Py_ssize_t ob_size - 1的操作。

這樣做的好處是,後面再新增資料項時,其實就不用再次進行擴容了。

但是在新增資料項之前,會判斷整個列表的容量是否過大,如果過大即代表還有很多空的位置,此時要進行縮容機制:

  • 如果資料項個數 + 1 < 當前列表容量的一半

    則進行縮容,刪除空的列表槽位

image-20210513104548039

如果是clear()清空元素,則直接非常乾脆的將容量以及大小都重置為0,並且將該列表所有槽位佔據的記憶體空間進行釋放。

遷徙機制

因為列表底層是順序儲存,必須佔用一個連續的記憶體空間。

如果在進行擴容時,發現後面連續的記憶體空間被其他物件所佔據,則會將整個列表進行一次拷貝。

然後遷徙到新的位置開闢記憶體,確保所有的列表槽位都是連續的。

image-20210513105108068

列表快取

當刪除一個列表之後,會將該列表中槽位引用的資料項地址全部清空。

並且將該列表的引用存放至一個叫做free_list的快取中,下次如果再需要建立列表,則直接從free_list快取中獲取。

  • free_list最多可以快取80個列表

示例,舊列表被刪除後將空列表的引用存放至free_list快取中,當再次建立一個新列表時,會直接從free_list快取中獲取舊列表,並且填入資料項:

li1 = [1, 2, 3]
print(id(li1))
del li1

li2 = [4, 5, 6, 7]
print(id(li2))

# 4405732936
# 4405732936

image-20210513111034466

PyObjectList.c原始碼

官網參考:點我跳轉

原始碼一覽:點我跳轉

以下是擷取了一些關鍵性原始碼,並且做上了中文註釋,方便查閱。

每一個列表都有幾個關鍵性的屬性:

Py_ssize_t ob_refcnt;     // 引用計數器
PyObject **ob_item;       // 列表內部槽位的資料項指標,即指標的指標
Py_ssize_t ob_size;       // 列表大小
Py_ssize_t allocated;     // 列表容量

建立列表

PyObject *
PyList_New(Py_ssize_t size)
{
    // 空列表
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }

    struct _Py_list_state *state = get_list_state();
    PyListObject *op;

#ifdef Py_DEBUG
    // PyList_New() must not be called after _PyList_Fini()
    assert(state->numfree != -1);
#endif

    // 判斷是否有free_list中是否有快取
    if (state->numfree) {
        // 有快取,free_list快取的列表個數減1
        state->numfree--;
        op = state->free_list[state->numfree];
        // 建立新的引用關係
        _Py_NewReference((PyObject *)op);
    }
    else {
        // 無快取,建立新列表,先開闢記憶體
        op = PyObject_GC_New(PyListObject, &PyList_Type);
        if (op == NULL) {
            return NULL;
        }
    }
    if (size <= 0) {
        // 如果列表是空的,則將ob_item設定為NULL,即不引用任何資料項
        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_SET_SIZE(op, size);
    // 設定列表的整體容量
    op->allocated = size;
    // 將列表放入雙向連結串列中以進行記憶體管理
    _PyObject_GC_TRACK(op);
    // 返回列表的結構體指標
    return (PyObject *) op;
}

新增元素

static int
app1(PyListObject *self, PyObject *v)
{
    // 獲取列表的大小(已佔用容量)
    Py_ssize_t n = PyList_GET_SIZE(self);

    assert (v != NULL);
    assert((size_t)n + 1 < PY_SSIZE_T_MAX);
    // 呼叫 list_resize()對容量進行計算
    // 擴容、或者縮容
    if (list_resize(self, n+1) < 0)
        return -1;

    // 增加資料項的引用計數器
    Py_INCREF(v);
    // 將資料項新增至列表的 ob_item 中
    PyList_SET_ITEM(self, n, v);
    return 0;
}

int
PyList_Append(PyObject *op, PyObject *newitem)
{
    // 傳入列表的引用, 還有新的資料項引用,並且驗證列表和資料項
    if (PyList_Check(op) && (newitem != NULL))
        // 進行新增
        return app1((PyListObject *)op, newitem);
    PyErr_BadInternalCall();
    return -1;
}


static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{

    PyObject **items;
    size_t new_allocated, num_allocated_bytes;
    Py_ssize_t allocated = self->allocated;

    /* Bypass realloc() when a previous overallocation is large enough
       to accommodate the newsize.  If the newsize falls lower than half
       the allocated size, then proceed with the realloc() to shrink the list.
    */

    // 擴容、縮容機制呼叫realloc()函式
    // allocated = 容量
    // newsize代表已存在的資料項個數 + 1

    // 當前列表容量 > 已有資料項個數+1 and 已有資料項個數 >=  當前列表容量的一半
    if (allocated >= newsize && newsize >= (allocated >> 1)) {
    
        assert(self->ob_item != NULL || newsize == 0);
        // 則直接新增資料項,不進行擴容
        // 並新增資料項個數 Py_ssize_t ob_size + 1
        Py_SET_SIZE(self, newsize);
        return 0;
    }

    /* This over-allocates proportional to the list size, making room
     * for additional growth.  The over-allocation is mild, but is
     * enough to give linear-time amortized behavior over a long
     * sequence of appends() in the presence of a poorly-performing
     * system realloc().
     * Add padding to make the allocated size multiple of 4.
     * The growth pattern is:  0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ...
     * Note: new_allocated won't overflow because the largest possible value
     *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
     */
    new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
    /* Do not overallocate if the new size is closer to overallocated size
     * than to the old size.
     */

    // 如果資料項個數 + 1 < 當前列表容量的一半,則進行縮容,刪除空的列表槽位
    // 當前列表容量 <  已有資料項個數+1,則先進行擴容後再新增資料項
    // 下面這句話是獲取新的容量值
    if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))
        new_allocated = ((size_t)newsize + 3) & ~(size_t)3;

    // 全是空的
    if (newsize == 0)
        new_allocated = 0;

    num_allocated_bytes = new_allocated * sizeof(PyObject *);

    // 基於realloc()進行擴容或者縮容,內部會包含資料項的位置調整
    items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
    if (items == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    // 重新設定列表的 ob_item、obsize、allocated
    self->ob_item = items;
    Py_SET_SIZE(self, newsize);
    self->allocated = new_allocated;
    return 0;
}

插入元素

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    // 獲取資料項個數
    Py_ssize_t i, n = Py_SIZE(self);
    PyObject **items;
    
    // 插入資料項確保不是NULL
    if (v == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }

    assert((size_t)n + 1 < PY_SSIZE_T_MAX);
    
    // 重新計算容量是否可容納新資料項的加入,從而進行擴容或縮容
    if (list_resize(self, n+1) < 0)
        return -1;
    
    // 如果where小於0,代表通過倒序索引進行插入
    if (where < 0) {
        where += n;
        if (where < 0)
            where = 0;
    }
   
   // 如果where大於列表的大小(已佔據容量),則where = 已佔據容量
   // 即插入到最後的一個位置
    if (where > n)
        where = n;
    
    // 拿到列表中所有資料項的引用
    items = self->ob_item;
    
    // 讓插入位置之後的所有資料項開始向後挪動1個位置,騰出位置來插入新的資料項
    for (i = n; --i >= where; )
        // i + 1指的是資料項指標地址,每次 - 1
        items[i+1] = items[i];
    
    // 新增資料項的引用計數 + 1
    Py_INCREF(v);
    
    // 新的資料項索引位置和值做繫結
    items[where] = v;
    return 0;
}

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 PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
// 傳入列表的引用,列表的彈出資料項索引位置

{
    PyObject *v;
    int status;

    // 如果是一個空列表,則丟擲異常
    if (Py_SIZE(self) == 0) {
        /* Special-case most common failure cause */
        PyErr_SetString(PyExc_IndexError, "pop from empty list");
        return NULL;
    }
    if (index < 0)
        index += Py_SIZE(self);
        
    // 如果索引值不存在,則丟擲異常
    if (!valid_index(index, Py_SIZE(self))) {
        PyErr_SetString(PyExc_IndexError, "pop index out of range");
        return NULL;
    }
    
    // 找到準備彈出的資料項位置
    v = self->ob_item[index];
    
    // 如果彈出的資料項是列表中的最後一個
    if (index == Py_SIZE(self) - 1) {
        // list_resize()內部只會做size - 1,而不會回收記憶體進行縮容
        status = list_resize(self, Py_SIZE(self) - 1);
        if (status >= 0)
            return v; /* and v now owns the reference the list had */
        else
            return NULL;
    }
    // 增加一次引用計數器
    Py_INCREF(v);
    
    // 如果彈出的資料項不是列表中的最後一個,則需要進行位置調整
    status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
    if (status < 0) {
        Py_DECREF(v);
        return NULL;
    }
    
    // 返回被彈出的資料項
    return v;
}

清空元素

static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        /* Because XDECREF can recursively invoke operations on
           this list, we make it empty first. */
        i = Py_SIZE(a);
        // 重新設定大小為0
        Py_SET_SIZE(a, 0);
        // 將列表中插槽引用的物件全部設定為None
        a->ob_item = NULL;
        // 重新設定容量為0
        a->allocated = 0;
        // 迴圈列表中的資料項,令所有資料項的引用計數-1
        while (--i >= 0) {
            Py_XDECREF(item[i]);
        }
        PyMem_Free(item);
    }
    /* Never fails; the return value can be ignored.
       Note that there is no guarantee that the list is actually empty
       at this point, because XDECREF may have populated it again! */
    return 0;
}

刪除列表

static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    
    // 內部會判斷這個列表是否還有其他識別符號引用,如果為0則代表沒有其他識別符號引用
    // 可以通過內部GC機制將該列表所佔據的記憶體空間進行釋放
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_BEGIN(op, list_dealloc)
    if (op->ob_item != NULL) {
        /* Do it backwards, for Christian Tismer.
           There's a simple test case where somehow this reduces
           thrashing when a *very* large list is created and
           immediately deleted. */
           
        // 獲取列表中已有資料項的個數(即大小)
        i = Py_SIZE(op);
        // 迴圈列表中的資料項,令所有資料項的引用計數-1
        while (--i >= 0) {
            Py_XDECREF(op->ob_item[i]);
        }
        PyMem_Free(op->ob_item);
    }
    struct _Py_list_state *state = get_list_state();
    
    
#ifdef Py_DEBUG
    // list_dealloc() must not be called after _PyList_Fini()
    assert(state->numfree != -1);
#endif

    // 判斷free_list中的已快取列表個數是否大於80,這裡是沒滿
    // 在free_list中新增空列表的引用即可
    if (state->numfree < PyList_MAXFREELIST && PyList_CheckExact(op)) {
        state->free_list[state->numfree++] = op;
    }
    else {
        // 如果free_list的大小已達到容量限制
        // 則直接在記憶體中銷燬列表的結構體物件
        Py_TYPE(op)->tp_free((PyObject *)op);
    }
    Py_TRASHCAN_END
}

相關文章