第四章 操作列表
切片也是对象,中括号则是列表的特殊方法
# 列表切片
>>> a = [1, 2, 3, 4, 5]
>>> a[1:-1]
[2, 3, 4]
>>> a[::-1]
[5, 4, 3, 2, 1]
切片是 python 的一种高级特性,通过它能够方便的对列表实现复杂的子列选取。虽然切片看上去是一种特别语法,但其实,切片本身也是一个对象操作。python 中有一个内置函数叫做 slice,当你使用如上的切片语法时,其实等同于你使用了一个 slice 函数生成一个切片对象。什么意思呢?
# 内置切片函数接受三个变量,第一个是 start,第二个是 end,第三个是步长。就如同切片一样。
# 通过它生成切片对象,一样能够操作列表
>>> slice(1, -1, 1)
slice(1, -1, 1)
>>> slice_obj = slice(1, -1, 1)
>>> type(slice_obj)
<class 'slice'>
>>> a[slice_obj] # 用切片对象而不是带冒号的语法来切片
[2, 3, 4]
>>> a[1:-1:1] # 效果等同的切片语法
[2, 3, 4]
# 更加本质的切片操作:列表的内置方法 __getitem__()
# 接受一个切片对象作为参数,从而对列表进行切片操作
# __getitem__() 等同于中括号!
>>> a.__getitem__(slice(1, -1, 1)) # 本质
[2, 3, 4]
>>> a[1:-1:1] # 实际写法
[2, 3, 4]
所以如同三大定理第一条,python 中的一切皆是对象。列表是对象,切片也是对象。列表的切片操作其实是在调用列表的内置特殊方法__getitem__()
,该方法接受一个切片对象作为参数,以此选择并返回列表中的对应元素。当然,实际情况中,你依然应该用切片语法list[start:end:step]
来进行切片操作,但通过上述解释,我们可以明白 python 中的很多特殊语法,其实只是基于 python 对象操作的简写!
万变不离其宗。
迭代
python中,许多类型都可以直接通过 for 循环进行迭代,比如列表和字符串。如果 python 是你学习的第一门编程语言,你可能不会觉得这有什么特殊的,但这其实是一个很酷的特性。如果你简单了解一下 c 语言中类似的循环,你就会发现,为什么我们说 python 是一门简洁易读,新手友好的语言。
>>> a = [1, 2, 3]
>>> for i in a:
... print(i)
...
1
2
3
>>> a = '123'
>>> for i in a:
... print(i)
...
1
2
3
但同时,不是所有的类型都可以通过 for 语句循环遍历的,比如说 int 就不行
>>> a = 123
>>> for i in a:
... # 这里会打印出 1,2,3 呢?还是会打印出 1 到 123 呢?
... print(i)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
# 答案是出错了!int 是不可迭代的。
如何解释 int 为什么不可以迭代呢?是因为会产生歧义吗?比如到底是迭代从 1-123 的所有数字,还是迭代 int 中出现的每一个字符(就和迭代 str 一样)呢?
其实,要理解深层次的原因,我们又要再次回到 python 中的对象及其方法这一基本定理来。上一节我们提到 python 中的很多特殊语法,其实只是基于 python 对象操作的简写。比如包在切片语法外围的中括号,其实就是调用 python 列表对象的__getitem__()
方法,而 for 循环也是这样!当你通过 for 循环语句去试图迭代一个对象时,其实 python 在内部调用了该对象的__iter__()
方法,iter 就是 iteration 的简写,只有当一个类型有__iter__()
方法时,它才可能被 for 循环遍历,而当它没有__iter__()
方法时,自然也就会产生不可迭代的错误了。
通过hasattr() 函数,我们可以证明这一点。这个内置函数能够让我们通过名称确认某个类型是否具有同名方法:
# 通过 hasattr() 查看某个类型是否具有名为 __iter__ 的方法
>>> hasattr(list, '__iter__')
True
>>> hasattr(str, '__iter__')
True
# int 没有 __iter__ 方法,所以 for 循环 int 就出错了
>>> hasattr(int, '__iter__')
False
列表拆分
考虑下面这样一个需求:
# 如下所示,把列表中 5 个元素分别分给 3 个变量
>>> a = [1, 2, 3, 4, 5]
# 目标
x1 = 1
x2 = [2, 3, 4]
x3 = 5
# 你会怎么做?
小学生写法
# 把列表中三个元素分别分给三个变量
>>> a = [1, 2, 3, 4, 5]
>>> x1 = a[0]
>>> x2 = a[1:4]
# 或者
>>> x2 = a[1:-1]
>>> x3 = a[2]
大学生写法
# 把列表中三个元素分别分给三个变量
>>> a = [1, 2, 3, 4, 5]
>>> x1, *x2, x3 = a
这里,我们就用到了列表拆分(unpack)。当一个表达式左边有多个变量,而右边是一个可迭代(根据上面的解释,也就是有__iter__()
方法)的对象时,python 就会自动拆分这个可迭代对象。但因为左边对象与右边元素数量不相等,我们就需要通过星号表达式(starred expression)来匹配中间的对象。上面这句语句的意思是,第一个和第三个变量只接受列表的第一个和最后一个元素,而中间带星号的变量接受所有其他元素(0 到任意数量个,并且中间变量始终为列表)
介绍一下列表推导式和 zip()
zip()
考虑下面这样一个需求:
# 将三个等长列表中的每一个数相加,得到一个新的列表
>>> a = [1, 2, 3]
>>> b = [3, 4, 5]
# 目标
>>> c = [4, 6, 8]
# 你会怎么做?
小学生
>>> a = [1, 2, 3]
>>> b = [3, 4, 5]
>>>
>>> c = []
>>>
>>> for pos in range(0, 3):
... i, j = a[pos], b[pos]
... # i 和 j 之所以能同时获得,原理也和上面的 list unpack 一样!
... cur_sum = i + j
... c.append(cur_sum)
>>> c
[4, 6, 8]
中学生
>>> a = [1, 2, 3]
>>> b = [3, 4, 5]
>>> c = []
>>>
>>> for i, j in zip(a, b):
... # i 和 j 是一对来自 a 和 b 中相同位置的值,如(1, 3)(2, 4) (3, 5)
... cur_sum = i + j
... c.append(cur_sum)
>>> c
[4, 6, 8]
zip 就是拉链,顾名思义,这个函数能够像拉链一样把两个列表一一对应起来,我们就不需要专门生成一个位置变量,而可以直接迭代他们的值了
大学生
>>> a = [1, 2, 3]
>>> b = [3, 4, 5]
>>> c = [i + j for i, j in zip(a, b)]
>>> c
[4, 6, 8]
# 😎
上面给 c 赋值的这个语法叫做列表推导式(list comprehension),它和第二种语法做的事情一模一样,只不过把生成列表与循环向列表赋值放在了一行里面完成!我们将在下一章结合 if 详细讨论列表推导式。但是这里,你可以看到,同一件事情,其实有很多不同的方式去完成。一般来说,我们都喜欢最简洁和优美的写法,也就是第三种!
学习建议:正确的名词 + Google 是最好的学习方式
很多时候,只要知道一个名词,剩下的事情,请教 google 就可以了!这里限于篇幅(懒),我没有详细展开上面提到的诸多名词,但既然你知道他们大概是什么了,你可以轻而易举的从 google 中获取更多他们的说明,以及精心设计的案例!
列表拆分(unpack)
星号表达式(starred expression)
对象的内置方法是许多语法形式的本质(如
__iter__()
),内置方法也叫魔法方法(magic methods, python 的魔力来源!) ,这是个很大的话题,有一本叫做 fluent python 的进阶书籍对我帮助很大!但这本书不太适合刚刚接触 python 的新手。列表推导式(list comprehension)
这些内容不仅仅是看上去简洁优美的奇技淫巧而已,在深入学习他们的过程中,我们也更能了解 python 的许多深刻特性(三大定理)。这对于理解后面的很多内容都是大有裨益的。
最后更新于
这有帮助吗?