-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7388673
commit a1858ee
Showing
2 changed files
with
335 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 虽然是设置元素,但删除元素也是调用的它,比如通过 <font color="blue">data[n: n+1]=[]</font> 便可删除索引为 n 的元素。事实上 remove 和 pop 方法都只是计算出待删除元素的索引,真正的删除操作还是通过 list_ass_slice 来执行的。 | ||
- 另外,当传入切片时,只有步长为 1,才会调用 list_slice 和 list_ass_slice。如果步长不为 1,那么就采用循环的方式逐个遍历。 | ||
|
||
## 小结 | ||
|
||
以上我们就介绍了列表作为序列型对象拥有的方法,但除了这些它还有很多自定义的方法。由于列表用得非常广泛,关于它的方法我们都来详细地说上一说,下一篇文章介绍列表的自定义方法。 | ||
|
||
------ | ||
|
||
| ||
|
||
**欢迎大家关注我的公众号:古明地觉的编程教室。** | ||
|
||
![](./images/qrcode_for_gh.jpg) | ||
|
||
**如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。** | ||
|
||
![](./images/supports.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters