楔子
列表拥有非常多的方法,比如添加元素、查询元素等,这些都属于列表的自定义方法。当然不光是列表,任何对象都可以有自己的自定义方法,而这些方法会保存在类型对象的 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__ 方法。
那么接下来我们就详细剖析一下这些方法的具体实现过程。
列表的相加
序列型对象都实现了加法运算,比如列表,两个列表相加可以合并为一个新的列表。
虽然使用了 + 操作符,但它在底层是由 tp_as_sequence.sq_concat 负责实现的,该字段被赋值为 list_concat 函数,看一下它的内部逻辑。
逻辑非常简单,假设两个列表 a 和 b 相加,过程如下。
- 先申请一个新列表,长度为 len(a) + len(b);
- 将列表 a 的元素拷贝到新列表中;
- 将列表 b 的元素拷贝到新列表中;
说白了就是两个 for 循环。
列表的重复
列表可以乘上一个整数,将自身重复指定次数,该过程会返回一个新列表。
虽然使用了 * 操作符,但它在底层是由 tp_as_sequence.sq_repeat 负责实现的,该字段被赋值为 list_repeat 函数,看一下它的内部逻辑。
假设列表 a 和整数 n 相乘,过程如下。
- 创建一个新列表,长度为 len(a) * n;
- 将列表 a 的元素拷贝到新列表中;
- 将新列表的元素再重复 n - 1 次;
基于索引和切片获取元素
列表可以基于索引和切片截取元素。
在底层它由 tp_as_mapping.mp_subscript 实现,该字段被赋值为 list_subscript 函数,看一下它的内部逻辑。
这个和之前介绍的 bytes 对象有点像,因为它们都是序列型对象,在基于索引和切片截取元素时的逻辑也是类似的。
但 bytes 对象只能截取元素,却不能设置元素,而列表是可以的,因为列表是可变对象。
基于索引和切片设置元素
列表是可变对象,因为它支持设置元素,即对内部元素进行修改。基于索引设置元素就不说了,我们主要看切片,它背后还是有一些复杂的。
至于它的源码有兴趣可以自己看一下,在底层它由 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 来执行的。
小结
以上我们就介绍了列表作为序列型对象拥有的方法,但除了这些它还有很多自定义的方法。