diff --git "a/src/36.\345\210\227\350\241\250\344\275\234\344\270\272\345\272\217\345\210\227\345\236\213\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\345\234\250\345\272\225\345\261\202\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237.md" "b/src/36.\345\210\227\350\241\250\344\275\234\344\270\272\345\272\217\345\210\227\345\236\213\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\345\234\250\345\272\225\345\261\202\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237.md" new file mode 100644 index 0000000..8e9c997 --- /dev/null +++ "b/src/36.\345\210\227\350\241\250\344\275\234\344\270\272\345\272\217\345\210\227\345\236\213\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\345\234\250\345\272\225\345\261\202\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237.md" @@ -0,0 +1,333 @@ +## 楔子 + +列表拥有非常多的方法,比如添加元素、查询元素等,这些都属于列表的自定义方法。当然不光是列表,任何对象都可以有自己的自定义方法,而这些方法会保存在类型对象的 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\_\_ 方法。 + +那么接下来我们就详细剖析一下这些方法的具体实现过程。 + +## 列表的相加 + +序列型对象都实现了加法运算,比如列表,两个列表相加可以合并为一个新的列表。 + +~~~Python +print([1, 2, 3] + [4, 5]) +""" +[1, 2, 3, 4, 5] +""" +~~~ + +虽然使用了 + 操作符,但它在底层是由 tp_as_sequence 的 sq_concat 负责实现的,该字段被赋值为 list_concat 函数,看一下它的内部逻辑。 + +~~~C +// 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", + bb->ob_type->tp_name); + return NULL; + } +#define b ((PyListObject *)bb) + // 两个列表的长度相加一定小于 PY_SSIZE_T_MAX + if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b)) + return PyErr_NoMemory(); + // 新列表的长度,等于相加的两个列表的长度之和 + size = Py_SIZE(a) + Py_SIZE(b); + // 为 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]; + Py_INCREF(v); + dest[i] = v; + } + // 将第二个列表的元素增加引用计数之后,拷贝到新列表中 + 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 设置为 size + Py_SIZE(np) = size; + // 转成泛型指针之后返回 + return (PyObject *)np; +#undef b +} +~~~ + +逻辑非常简单,假设两个列表 a 和 b 相加,过程如下。 + +- 先申请一个新列表,长度为 len(a) + len(b); +- 将列表 a 的元素拷贝到新列表中; +- 将列表 b 的元素拷贝到新列表中; + +说白了就是两个 for 循环。 + +## 列表的重复 + +列表可以乘上一个整数,将自身重复指定次数,该过程会返回一个新列表。 + +```python +print([1, 2, 3] * 3) +""" +[1, 2, 3, 1, 2, 3, 1, 2, 3] +""" +``` + +虽然使用了 * 操作符,但它在底层是由 tp_as_sequence 的 sq_repeat 负责实现的,该字段被赋值为 list_repeat 函数,看一下它的内部逻辑。 + +~~~C +// Objects/listobject.c +static PyObject * +list_repeat(PyListObject *a, Py_ssize_t n) +{ + Py_ssize_t i, j; + Py_ssize_t size; + PyListObject *np; + PyObject **p, **items; + PyObject *elem; + // 如果 n 小于 0,那么将 n 设置为 0 + if (n < 0) + n = 0; + // 长度有限制,不能超过 PY_SSIZE_T_MAX + if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n) + return PyErr_NoMemory(); + // 新列表的长度 + size = Py_SIZE(a) * n; + // 如果列表长度为 0,那么直接返回空列表即可 + if (size == 0) + return PyList_New(0); + // 为新列表和底层数组申请空间,底层数组的长度为 size + np = (PyListObject *) list_new_prealloc(size); + if (np == NULL) + return NULL; + // 如果原始列表的长度为 1,比如 a = [1],n = 3 + // 那么新列表就是 [1, 1, 1] + if (Py_SIZE(a) == 1) { + // 指向新列表的底层数组的首元素 + items = np->ob_item; + // 拿到原始列表的第一个元素 + elem = a->ob_item[0]; + // 将新列表的底层数组的元素全部设置为 elem + for (i = 0; i < n; i++) { + items[i] = elem; + Py_INCREF(elem); + } + } + // 如果原始列表的长度不为 1 + else { + // 指向新列表的底层数组的首元素 + p = np->ob_item; + // 指向原始列表的底层数组的首元素 + items = a->ob_item; + // 两层 for 循环 + // 内层循环遍历原始数组,将元素拷贝到新数组,外层循环则是循环 n 次 + for (i = 0; i < n; i++) { + for (j = 0; j < Py_SIZE(a); j++) { + *p = items[j]; + Py_INCREF(*p); + p++; + } + } + } + // 将新列表的 ob_size 设置为 size + Py_SIZE(np) = size; + return (PyObject *) np; +} +~~~ + +整个过程非常朴实无华。 + +## 基于索引和切片获取元素 + +列表可以基于索引和切片截取元素。 + +~~~Python +data = [1, 2, 3, 4, 5] +print(data[1]) # 2 +print(data[1: 4]) # [2, 3, 4] +~~~ + +在底层它由 tp_as_mapping 的 mp_subscript 实现,该字段被赋值为 list_subscript 函数,看一下它的内部逻辑。 + +~~~C +// Objects/listobject.c + +static PyObject * +list_subscript(PyListObject* self, PyObject* item) +{ + // 在基于索引和切片截取时,所有序列型对象的逻辑都差不多 + if (PyIndex_Check(item)) { + Py_ssize_t i; + // 如果 item 是索引,那么转成 Py_ssize_t 整数 + 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, cur, i; + 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; + // 从 start 处开始遍历,将元素拷贝过去 + // 然后 cur 每次增加 step,遍历次数为 slicelength + dest = ((PyListObject *)result)->ob_item; + for (cur = start, i = 0; i < slicelength; + cur += (size_t)step, i++) { + it = src[cur]; + Py_INCREF(it); + dest[i] = it; + } + // 将新列表的 ob_size 设置为 slicelength + Py_SIZE(result) = slicelength; + return result; + } + } + // 否则说明 item 既不是索引也不是切片,那么报错 + else { + PyErr_Format(PyExc_TypeError, + "list indices must be integers or slices, not %.200s", + item->ob_type->tp_name); + return NULL; + } +} +~~~ + +这个和之前介绍的 bytes 对象有点像,因为它们都是序列型对象,在基于索引和切片截取元素时的逻辑也是类似的。但 bytes 对象只能截取元素,却不能设置元素,而列表是可以的,因为列表是可变对象。 + +## 基于索引和切片设置元素 + +列表是可变对象,因为它支持设置元素,即对内部元素进行修改。基于索引设置元素就不说了,我们主要看切片,它背后还是有一些复杂的。 + +~~~Python +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,那么就采用循环的方式逐个遍历。 + +## 小结 + +以上我们就介绍了列表作为序列型对象拥有的方法,但除了这些它还有很多自定义的方法。由于列表用得非常广泛,关于它的方法我们都来详细地说上一说,下一篇文章介绍列表的自定义方法。 + +------ + +  + +**欢迎大家关注我的公众号:古明地觉的编程教室。** + +![](./images/qrcode_for_gh.jpg) + +**如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。** + +![](./images/supports.png) \ No newline at end of file diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 6c156e8..a078945 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -35,4 +35,5 @@ + [32. 聊一聊字符串常见操作的源码实现](32.聊一聊字符串常见操作的源码实现.md) + [33. 列表是怎么实现的?解密列表的数据结构](33.列表是怎么实现的?解密列表的数据结构.md) + [34. 列表是怎么扩容的?](34.列表是怎么扩容的?.md) -+ [35. 解密列表的创建与销毁,以及缓存池长什么样子?](35.解密列表的创建与销毁,以及缓存池长什么样子?.md) \ No newline at end of file ++ [35. 解密列表的创建与销毁,以及缓存池长什么样子?](35.解密列表的创建与销毁,以及缓存池长什么样子?.md) ++ [36. 列表作为序列型对象都支持哪些操作,它们在底层是怎么实现的?](36.列表作为序列型对象都支持哪些操作,它们在底层是怎么实现的?.md) \ No newline at end of file