列表作為序列型對(duì)象都支持哪些操作,它們?cè)诘讓邮窃趺磳?shí)現(xiàn)的?
楔子
列表?yè)碛蟹浅6嗟姆椒?,比如添加元素、查詢?cè)氐?,這些都屬于列表的自定義方法。當(dāng)然不光是列表,任何對(duì)象都可以有自己的自定義方法,而這些方法會(huì)保存在類型對(duì)象的 tp_methods 里面。
圖片
當(dāng)然列表除了擁有自定義的方法之外,還擁有作為序列型對(duì)象所共有的方法,比如合并、基于索引和切片獲取元素、基于索引和切片設(shè)置元素等等。
圖片
這些方法會(huì)基于種類被抽象成三個(gè)方法簇,分別是:
- tp_as_number:數(shù)值型對(duì)象擁有的方法;
- tp_as_sequence:序列型對(duì)象擁有的方法;
- tp_as_mapping:映射型對(duì)象擁有的方法;
每個(gè)方法簇都包含了大量的 C 函數(shù),每個(gè) C 函數(shù)一般會(huì)對(duì)應(yīng) Python 里的一個(gè)魔法方法和操作符。比如 tp_as_sequence 的 sq_concat 對(duì)應(yīng)序列型對(duì)象的 __add__ 方法,tp_as_number 的 nb_subtract 對(duì)應(yīng)數(shù)值型對(duì)象的 __sub__ 方法。
那么接下來(lái)我們就詳細(xì)剖析一下這些方法的具體實(shí)現(xiàn)過(guò)程。
列表的相加
序列型對(duì)象都實(shí)現(xiàn)了加法運(yùn)算,比如列表,兩個(gè)列表相加可以合并為一個(gè)新的列表。
print([1, 2, 3] + [4, 5])
"""
[1, 2, 3, 4, 5]
"""
雖然使用了 + 操作符,但它在底層是由 tp_as_sequence.sq_concat 負(fù)責(zé)實(shí)現(xiàn)的,該字段被賦值為 list_concat 函數(shù),看一下它的內(nèi)部邏輯。
// Objects/listobject.c
static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
// 兩個(gè)列表相加之后的新列表的長(zhǎng)度
Py_ssize_t size;
Py_ssize_t i;
PyObject **src, **dest;
PyListObject *np;
// 如果 bb 不是列表,拋出 TypeError
if (!PyList_Check(bb)) {
PyErr_Format(PyExc_TypeError,
"can only concatenate list (not \"%.200s\") to list",
Py_TYPE(bb)->tp_name);
return NULL;
}
#define b ((PyListObject *)bb)
// 兩個(gè)列表的長(zhǎng)度相加一定小于 PY_SSIZE_T_MAX
assert((size_t)Py_SIZE(a) + (size_t)Py_SIZE(b) < PY_SSIZE_T_MAX);
// 新列表的長(zhǎng)度,等于相加的兩個(gè)列表的長(zhǎng)度之和
size = Py_SIZE(a) + Py_SIZE(b);
// 如果長(zhǎng)度為 0,那么創(chuàng)建一個(gè)空列表,然后返回即可
if (size == 0) {
return PyList_New(0);
}
// 為 PyListObject 和底層數(shù)組申請(qǐng)空間(空間大小為 8 * size)
np = (PyListObject *) list_new_prealloc(size);
if (np == NULL) {
return NULL;
}
// 將第一個(gè)列表的元素增加引用計(jì)數(shù)之后,拷貝到新列表中
src = a->ob_item;
dest = np->ob_item;
for (i = 0; i < Py_SIZE(a); i++) {
PyObject *v = src[i];
dest[i] = Py_NewRef(v);
}
// 將第二個(gè)列表的元素增加引用計(jì)數(shù)之后,拷貝到新列表中
src = b->ob_item;
dest = np->ob_item + Py_SIZE(a);
for (i = 0; i < Py_SIZE(b); i++) {
PyObject *v = src[i];
dest[i] = Py_NewRef(v);
}
// 將新列表的 ob_size 設(shè)置為 size
Py_SET_SIZE(np, size);
// 轉(zhuǎn)成泛型指針之后返回
return (PyObject *)np;
#undef b
}
邏輯非常簡(jiǎn)單,假設(shè)兩個(gè)列表 a 和 b 相加,過(guò)程如下。
- 先申請(qǐng)一個(gè)新列表,長(zhǎng)度為 len(a) + len(b);
- 將列表 a 的元素拷貝到新列表中;
- 將列表 b 的元素拷貝到新列表中;
說(shuō)白了就是兩個(gè) for 循環(huán)。
列表的重復(fù)
列表可以乘上一個(gè)整數(shù),將自身重復(fù)指定次數(shù),該過(guò)程會(huì)返回一個(gè)新列表。
print([1, 2, 3] * 3)
"""
[1, 2, 3, 1, 2, 3, 1, 2, 3]
"""
雖然使用了 * 操作符,但它在底層是由 tp_as_sequence.sq_repeat 負(fù)責(zé)實(shí)現(xiàn)的,該字段被賦值為 list_repeat 函數(shù),看一下它的內(nèi)部邏輯。
// Objects/listobject.c
static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
// 獲取 a 指向的列表的長(zhǎng)度
const Py_ssize_t input_size = Py_SIZE(a);
// 如果列表長(zhǎng)度為 0,或者乘上一個(gè)小于等于 0 的數(shù)
// 那么創(chuàng)建的新列表的長(zhǎng)度依舊是 0,因此直接返回空列表即可
if (input_size == 0 || n <= 0)
return PyList_New(0);
assert(n > 0);
// 長(zhǎng)度有限制,不能超過(guò) PY_SSIZE_T_MAX
if (input_size > PY_SSIZE_T_MAX / n)
return PyErr_NoMemory();
// 新列表的長(zhǎng)度
Py_ssize_t output_size = input_size * n;
// 為新列表和底層數(shù)組申請(qǐng)空間,底層數(shù)組的長(zhǎng)度為 output_size
PyListObject *np = (PyListObject *) list_new_prealloc(output_size);
if (np == NULL)
return NULL;
// 指向新列表的底層數(shù)組的首元素
PyObject **dest = np->ob_item;
// 如果原始列表的長(zhǎng)度為 1,比如 a = [1],n = 3
// 那么新列表就是 [1, 1, 1]
if (input_size == 1) {
// 拿到第一個(gè)元素
PyObject *elem = a->ob_item[0];
// 因?yàn)橐貜?fù) n 次,所以引用計(jì)數(shù)增加 n
_Py_RefcntAdd(elem, n);
PyObject **dest_end = dest + output_size;
// 將新列表的底層數(shù)組的元素全部設(shè)置為 elem
while (dest < dest_end) {
*dest++ = elem;
}
}
// 如果原始列表的長(zhǎng)度不為 1
else {
PyObject **src = a->ob_item;
PyObject **src_end = src + input_size;
// 將原始列表的元素依次拷貝到新列表中,但此時(shí)只拷貝了 1 次
while (src < src_end) {
_Py_RefcntAdd(*src, n);
*dest++ = *src++;
}
// 將新列表里面的元素再重復(fù) output_size / input_size - 1 次
// 說(shuō)白了就是再重復(fù) n - 1 次
_Py_memory_repeat((char *)np->ob_item,
sizeof(PyObject *)*output_size,
sizeof(PyObject *)*input_size);
}
// 將新列表的 ob_size 設(shè)置為 output_size
Py_SET_SIZE(np, output_size);
return (PyObject *) np;
}
假設(shè)列表 a 和整數(shù) n 相乘,過(guò)程如下。
- 創(chuàng)建一個(gè)新列表,長(zhǎng)度為 len(a) * n;
- 將列表 a 的元素拷貝到新列表中;
- 將新列表的元素再重復(fù) n - 1 次;
基于索引和切片獲取元素
列表可以基于索引和切片截取元素。
data = [1, 2, 3, 4, 5]
print(data[1]) # 2
print(data[1: 4]) # [2, 3, 4]
在底層它由 tp_as_mapping.mp_subscript 實(shí)現(xiàn),該字段被賦值為 list_subscript 函數(shù),看一下它的內(nèi)部邏輯。
// Objects/listobject.c
static PyObject *
list_subscript(PyListObject* self, PyObject* item)
{
// 在基于索引和切片截取時(shí),所有序列型對(duì)象的邏輯都差不多
if (_PyIndex_Check(item)) {
// 如果 item 是索引,那么轉(zhuǎn)成 Py_ssize_t 整數(shù)
Py_ssize_t i;
i = PyNumber_AsSsize_t(item, PyExc_IndexError);
if (i == -1 && PyErr_Occurred())
return NULL;
// 如果 i 小于 0,那么加上列表長(zhǎng)度,轉(zhuǎn)成正數(shù)索引
if (i < 0)
i += PyList_GET_SIZE(self);
// 調(diào)用 list_item 獲取 ob_item 中索引為 i 的元素
return list_item(self, i);
}
// 如果 item 是切片
else if (PySlice_Check(item)) {
// start, stop, step 分別表示起始位置、終止位置、步長(zhǎng)
// slicelength 表示切片截取的長(zhǎng)度,也就是要截取多少個(gè)元素
Py_ssize_t start, stop, step, slicelength, i;
size_t cur;
PyObject* result;
PyObject* it;
PyObject **src, **dest;
// 獲取切片的 start、stop、step
if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
return NULL;
}
// 傳入原始列表的長(zhǎng)度,對(duì) start 和 stop 進(jìn)行調(diào)整,并返回 slicelength
slicelength = PySlice_AdjustIndices(Py_SIZE(self), &start, &stop,
step);
// 如果 slicelength <= 0,說(shuō)明截取不到任何元素
// 比如 data[5: 1] 或者 data[1: 5: -1],那么直接返回空列表
if (slicelength <= 0) {
return PyList_New(0);
}
// 如果步長(zhǎng)為 1,那么直接將列表中 start 到 stop 之間的元素拷過(guò)去即可
else if (step == 1) {
return list_slice(self, start, stop);
}
// 否則說(shuō)明步長(zhǎng)不為 1
else {
// 為創(chuàng)建的新列表和底層數(shù)組申請(qǐng)空間
result = list_new_prealloc(slicelength);
if (!result) return NULL;
src = self->ob_item;
dest = ((PyListObject *)result)->ob_item;
// 從 start 處開(kāi)始遍歷,將元素拷貝過(guò)去
// 然后 cur 每次增加 step,遍歷次數(shù)為 slicelength
for (cur = start, i = 0; i < slicelength;
cur += (size_t)step, i++) {
it = Py_NewRef(src[cur]);
dest[i] = it;
}
// 將新列表的 ob_size 設(shè)置為 slicelength
Py_SET_SIZE(result, slicelength);
return result;
}
}
// 否則說(shuō)明 item 既不是索引也不是切片,那么報(bào)錯(cuò)
else {
PyErr_Format(PyExc_TypeError,
"list indices must be integers or slices, not %.200s",
Py_TYPE(item)->tp_name);
return NULL;
}
}
這個(gè)和之前介紹的 bytes 對(duì)象有點(diǎn)像,因?yàn)樗鼈兌际切蛄行蛯?duì)象,在基于索引和切片截取元素時(shí)的邏輯也是類似的。
但 bytes 對(duì)象只能截取元素,卻不能設(shè)置元素,而列表是可以的,因?yàn)榱斜硎强勺儗?duì)象。
基于索引和切片設(shè)置元素
列表是可變對(duì)象,因?yàn)樗С衷O(shè)置元素,即對(duì)內(nèi)部元素進(jìn)行修改?;谒饕O(shè)置元素就不說(shuō)了,我們主要看切片,它背后還是有一些復(fù)雜的。
data = [1, 2, 3, 4, 5, 6, 7, 8]
# 通過(guò)切片設(shè)置元素,右值一定是一個(gè)可迭代對(duì)象
data[0: 3] = [11, 22, 33]
# 會(huì)將 data[0] 設(shè)置為 11,將 data[1] 設(shè)置為 22,將 data[2] 設(shè)置為 33
print(data)
"""
[11, 22, 33, 4, 5, 6, 7, 8]
"""
# 而且它們的長(zhǎng)度是可以不相等的,這里表示將 [0: 3] 的元素設(shè)置為 [1, 2]
# 即 data[0] 設(shè)置成 1,data[1] 設(shè)置成 2,那么問(wèn)題來(lái)了,data[2] 咋辦?
# 由于右值中已經(jīng)沒(méi)有元素與之匹配了,那么 data[2] 就會(huì)被刪掉
data[0: 3] = [1, 2]
print(data)
"""
[1, 2, 4, 5, 6, 7, 8]
"""
# 所以如果想刪除 [0: 3] 的元素,那么只需要執(zhí)行 data[0: 3] = [] 即可
# 因?yàn)?[] 里面沒(méi)有元素能與之匹配,所以 data 中 [0: 3] 的位置由于匹配不到
# 那么相當(dāng)于執(zhí)行了刪除操作,當(dāng)然由于 Python 的動(dòng)態(tài)特性,還可以像下面這么做
# data[0: 3] = []、data[0: 3] = ()、data[0: 3] = "" 等等也是沒(méi)問(wèn)題的
data[0: 3] = ""
print(data)
"""
[5, 6, 7, 8]
"""
# 實(shí)際上執(zhí)行 del data[0] 的時(shí)候,就是執(zhí)行了 data[0: 1] = []
# 當(dāng)然,如果右值元素多的話也是可以的,相當(dāng)于插入
# 比如這里的 data[0] 匹配 1,然后左邊就結(jié)束了
# 于是右側(cè)剩余的元素會(huì)依次插在后面
data[0: 1] = [1, 2, 3, 4]
print(data)
"""
[1, 2, 3, 4, 6, 7, 8]
"""
# 重點(diǎn)來(lái)了,如果切片的步長(zhǎng)不等于 1 的話,那么兩邊一定要匹配
# 由于 data[:: 2] 會(huì)得到 4 個(gè)元素,那么右邊的可迭代對(duì)象的長(zhǎng)度就必須也是 4
data[:: 2] = ['a', 'b', 'c', 'd']
print(data)
"""
['a', 2, 'b', 4, 'c', 7, 'd']
"""
# 但如果長(zhǎng)度不一致,那么會(huì)報(bào)錯(cuò)
try:
data[:: 2] = ['a', 'b', 'c']
except Exception as e:
# 顯然會(huì)報(bào)錯(cuò)
print(e)
"""
attempt to assign sequence of size 3 to extended slice of size 4
"""
至于它的源碼有興趣可以自己看一下,在底層它由 tp_as_mapping.mp_ass_subscript 負(fù)責(zé)實(shí)現(xiàn),該字段被賦值為 list_ass_subscript 函數(shù)。邏輯比較長(zhǎng),但不難理解,我們總結(jié)一下。
list_subscript 用于獲取元素,list_ass_subscript 用于設(shè)置元素。調(diào)用這兩個(gè)函數(shù),我們即可以傳入索引,也可以傳入切片。
- 獲取元素時(shí)傳入的是索引,那么 list_subscript 內(nèi)部會(huì)調(diào)用 list_item,傳入的是切片,那么會(huì)調(diào)用 list_slice。
- 設(shè)置元素時(shí)傳入的是索引,那么 list_ass_subscript 內(nèi)部會(huì)調(diào)用 list_ass_item,傳入的是切片,那么會(huì)調(diào)用 list_ass_slice。并且 list_ass_slice 雖然是設(shè)置元素,但刪除元素也是調(diào)用的它,比如通過(guò) data[n:n+1]=[] 便可刪除索引為 n 的元素。事實(shí)上 remove 和 pop 方法都只是計(jì)算出待刪除元素的索引,真正的刪除操作還是通過(guò) list_ass_slice 來(lái)執(zhí)行的。
小結(jié)
以上我們就介紹了列表作為序列型對(duì)象擁有的方法,但除了這些它還有很多自定義的方法。