楔子
列表拥有非常多的方法,比如添加元素、查询元素等,这些都属于列表的自定义方法。当然不光是列表,任何对象都可以有自己的自定义方法,而这些方法会保存在类型对象的 tp_methods 里面。
当然列表除了拥有自定义的方法之外,还拥有作为序列型对象所共有的方法,比如合并、基于索引和切片获取元素、基于索引和切片设置元素等等。
这些方法会基于种类被抽象成三个方法簇,分别是:
tp_as_number:数值型对象拥有的方法;
tp_as_sequence:序列型对象拥有的方法;
tp_as_mapping:映射型对象拥有的方法;
每个方法簇都包含了大量的 C 函数,每个 C 函数一般会对应 Python 里的一个魔法方法和操作符。比如 tp_as_sequence 的 sq_concat 对应序列型对象的 __add__ 方法,tp_as_number 的 nb_subtract 对应数值型对象的 __sub__ 方法。
那么接下来我们就详细剖析一下这些方法的具体实现过程。
列表的相加
序列型对象都实现了加法运算,比如列表,两个列表相加可以合并为一个新的列表。
print([1, 2, 3] + [4, 5])
"""
[1, 2, 3, 4, 5]
"""
虽然使用了 + 操作符,但它在底层是由 tp_as_sequence.sq_concat 负责实现的,该字段被赋值为 list_concat 函数,看一下它的内部逻辑。
// Objects/listobject.c
static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
// 两个列表相加之后的新列表的长度
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)
// 两个列表的长度相加一定小于 PY_SSIZE_T_MAX
assert((size_t)Py_SIZE(a) + (size_t)Py_SIZE(b) < PY_SSIZE_T_MAX);
// 新列表的长度,等于相加的两个列表的长度之和
size = Py_SIZE(a) + Py_SIZE(b);
// 如果长度为 0,那么创建一个空列表,然后返回即可
if (size == 0) {
return PyList_New(0);
}
// 为 PyListObject 和底层数组申请空间(空间大小为 8 * size)
np = (PyListObject *) list_new_prealloc(size);
if (np == NULL) {
return NULL;
}
// 将第一个列表的元素增加引用计数之后,拷贝到新列表中
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);
}
// 将第二个列表的元素增加引用计数之后,拷贝到新列表中
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 设置为 size
Py_SET_SIZE(np, size);
// 转成泛型指针之后返回
return (PyObject *)np;
#undef b
}
逻辑非常简单,假设两个列表 a 和 b 相加,过程如下。
先申请一个新列表,长度为 len(a) + len(b);
将列表 a 的元素拷贝到新列表中;
将列表 b 的元素拷贝到新列表中;
说白了就是两个 for 循环。
列表的重复
列表可以乘上一个整数,将自身重复指定次数,该过程会返回一个新列表。
print([1, 2, 3] * 3)
"""
[1, 2, 3, 1, 2, 3, 1, 2, 3]
"""
虽然使用了 * 操作符,但它在底层是由 tp_as_sequence.sq_repeat 负责实现的,该字段被赋值为 list_repeat 函数,看一下它的内部逻辑。
// Objects/listobject.c
static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
// 获取 a 指向的列表的长度
const Py_ssize_t input_size = Py_SIZE(a);
// 如果列表长度为 0,或者乘上一个小于等于 0 的数
// 那么创建的新列表的长度依旧是 0,因此直接返回空列表即可
if (input_size == 0 || n <= 0)
return PyList_New(0);
assert(n > 0);
// 长度有限制,不能超过 PY_SSIZE_T_MAX
if (input_size > PY_SSIZE_T_MAX / n)
return PyErr_NoMemory();
// 新列表的长度
Py_ssize_t output_size = input_size * n;
// 为新列表和底层数组申请空间,底层数组的长度为 output_size
PyListObject *np = (PyListObject *) list_new_prealloc(output_size);
if (np == NULL)
return NULL;
// 指向新列表的底层数组的首元素
PyObject **dest = np->ob_item;
// 如果原始列表的长度为 1,比如 a = [1],n = 3
// 那么新列表就是 [1, 1, 1]
if (input_size == 1) {
// 拿到第一个元素
PyObject *elem = a->ob_item[0];
// 因为要重复 n 次,所以引用计数增加 n
_Py_RefcntAdd(elem, n);
PyObject **dest_end = dest + output_size;
// 将新列表的底层数组的元素全部设置为 elem
while (dest < dest_end) {
*dest++ = elem;
}
}
// 如果原始列表的长度不为 1
else {
PyObject **src = a->ob_item;
PyObject **src_end = src + input_size;
// 将原始列表的元素依次拷贝到新列表中,但此时只拷贝了 1 次
while (src < src_end) {
_Py_RefcntAdd(*src, n);
*dest++ = *src++;
}
// 将新列表里面的元素再重复 output_size / input_size - 1 次
// 说白了就是再重复 n - 1 次
_Py_memory_repeat((char *)np->ob_item,
sizeof(PyObject *)*output_size,
sizeof(PyObject *)*input_size);
}
// 将新列表的 ob_size 设置为 output_size
Py_SET_SIZE(np, output_size);
return (PyObject *) np;
}
假设列表 a 和整数 n 相乘,过程如下。
创建一个新列表,长度为 len(a) * n;
将列表 a 的元素拷贝到新列表中;
将新列表的元素再重复 n - 1 次;
基于索引和切片获取元素
列表可以基于索引和切片截取元素。
data = [1, 2, 3, 4, 5]
print(data[1]) # 2
print(data[1: 4]) # [2, 3, 4]
在底层它由 tp_as_mapping.mp_subscript 实现,该字段被赋值为 list_subscript 函数,看一下它的内部逻辑。
// Objects/listobject.c
static PyObject *
list_subscript(PyListObject* self, PyObject* item)
{
// 在基于索引和切片截取时,所有序列型对象的逻辑都差不多
if (_PyIndex_Check(item)) {
// 如果 item 是索引,那么转成 Py_ssize_t 整数
Py_ssize_t i;
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 获取 ob_item 中索引为 i 的元素
return list_item(self, i);
}
// 如果 item 是切片
else if (PySlice_Check(item)) {
// start, stop, step 分别表示起始位置、终止位置、步长
// slicelength 表示切片截取的长度,也就是要截取多少个元素
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;
}
// 传入原始列表的长度,对 start 和 stop 进行调整,并返回 slicelength
slicelength = PySlice_AdjustIndices(Py_SIZE(self), &start, &stop,
step);
// 如果 slicelength <= 0,说明截取不到任何元素
// 比如 data[5: 1] 或者 data[1: 5: -1],那么直接返回空列表
if (slicelength <= 0) {
return PyList_New(0);
}
// 如果步长为 1,那么直接将列表中 start 到 stop 之间的元素拷过去即可
else if (step == 1) {
return list_slice(self, start, stop);
}
// 否则说明步长不为 1
else {
// 为创建的新列表和底层数组申请空间
result = list_new_prealloc(slicelength);
if (!result) return NULL;
src = self->ob_item;
dest = ((PyListObject *)result)->ob_item;
// 从 start 处开始遍历,将元素拷贝过去
// 然后 cur 每次增加 step,遍历次数为 slicelength
for (cur = start, i = 0; i < slicelength;
cur += (size_t)step, i++) {
it = Py_NewRef(src[cur]);
dest[i] = it;
}
// 将新列表的 ob_size 设置为 slicelength
Py_SET_SIZE(result, slicelength);
return result;
}
}
// 否则说明 item 既不是索引也不是切片,那么报错
else {
PyErr_Format(PyExc_TypeError,
"list indices must be integers or slices, not %.200s",
Py_TYPE(item)->tp_name);
return NULL;
}
}
这个和之前介绍的 bytes 对象有点像,因为它们都是序列型对象,在基于索引和切片截取元素时的逻辑也是类似的。
但 bytes 对象只能截取元素,却不能设置元素,而列表是可以的,因为列表是可变对象。
基于索引和切片设置元素
列表是可变对象,因为它支持设置元素,即对内部元素进行修改。基于索引设置元素就不说了,我们主要看切片,它背后还是有一些复杂的。
data = [1, 2, 3, 4, 5, 6, 7, 8]
# 通过切片设置元素,右值一定是一个可迭代对象
data[0: 3] = [11, 22, 33]
# 会将 data[0] 设置为 11,将 data[1] 设置为 22,将 data[2] 设置为 33
print(data)
"""
[11, 22, 33, 4, 5, 6, 7, 8]
"""
# 而且它们的长度是可以不相等的,这里表示将 [0: 3] 的元素设置为 [1, 2]
# 即 data[0] 设置成 1,data[1] 设置成 2,那么问题来了,data[2] 咋办?
# 由于右值中已经没有元素与之匹配了,那么 data[2] 就会被删掉
data[0: 3] = [1, 2]
print(data)
"""
[1, 2, 4, 5, 6, 7, 8]
"""
# 所以如果想删除 [0: 3] 的元素,那么只需要执行 data[0: 3] = [] 即可
# 因为 [] 里面没有元素能与之匹配,所以 data 中 [0: 3] 的位置由于匹配不到
# 那么相当于执行了删除操作,当然由于 Python 的动态特性,还可以像下面这么做
# data[0: 3] = []、data[0: 3] = ()、data[0: 3] = "" 等等也是没问题的
data[0: 3] = ""
print(data)
"""
[5, 6, 7, 8]
"""
# 实际上执行 del data[0] 的时候,就是执行了 data[0: 1] = []
# 当然,如果右值元素多的话也是可以的,相当于插入
# 比如这里的 data[0] 匹配 1,然后左边就结束了
# 于是右侧剩余的元素会依次插在后面
data[0: 1] = [1, 2, 3, 4]
print(data)
"""
[1, 2, 3, 4, 6, 7, 8]
"""
# 重点来了,如果切片的步长不等于 1 的话,那么两边一定要匹配
# 由于 data[:: 2] 会得到 4 个元素,那么右边的可迭代对象的长度就必须也是 4
data[:: 2] = ['a', 'b', 'c', 'd']
print(data)
"""
['a', 2, 'b', 4, 'c', 7, 'd']
"""
# 但如果长度不一致,那么会报错
try:
data[:: 2] = ['a', 'b', 'c']
except Exception as e:
# 显然会报错
print(e)
"""
attempt to assign sequence of size 3 to extended slice of size 4
"""
至于它的源码有兴趣可以自己看一下,在底层它由 tp_as_mapping.mp_ass_subscript 负责实现,该字段被赋值为 list_ass_subscript 函数。逻辑比较长,但不难理解,我们总结一下。
list_subscript 用于获取元素,list_ass_subscript 用于设置元素。调用这两个函数,我们即可以传入索引,也可以传入切片。
获取元素时传入的是索引,那么 list_subscript 内部会调用 list_item,传入的是切片,那么会调用 list_slice。
设置元素时传入的是索引,那么 list_ass_subscript 内部会调用 list_ass_item,传入的是切片,那么会调用 list_ass_slice。并且 list_ass_slice 虽然是设置元素,但删除元素也是调用的它,比如通过 data[n:n+1]=[] 便可删除索引为 n 的元素。事实上 remove 和 pop 方法都只是计算出待删除元素的索引,真正的删除操作还是通过 list_ass_slice 来执行的。
另外,当传入切片时,只有步长为 1,才会调用 list_slice 和 list_ass_slice。如果步长不为 1,那么就采用循环的方式逐个遍历。
小结
以上我们就介绍了列表作为序列型对象拥有的方法,但除了这些它还有很多自定义的方法。由于列表用得非常广泛,关于它的方法我们都来详细地说上一说,下一篇文章介绍列表的自定义方法。