第四章 操作列表

切片也是对象,中括号则是列表的特殊方法

# 列表切片
>>> 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

小问题:既然 int 不可循环,那我们怎么实现用 for 循环 1 - 123 的每一个整数呢?

列表拆分

考虑下面这样一个需求:

# 如下所示,把列表中 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 到任意数量个,并且中间变量始终为列表)

星号表达式很重要,你知道这个名词之后,可以通过 google 学到更多细节。或者,就留个印象也是很好的,因为在函数章节我们还要提到它。

介绍一下列表推导式和 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 的许多深刻特性(三大定理)。这对于理解后面的很多内容都是大有裨益的

比如,星号表达式就会在函数参数传递中再次讲到,可迭代对象也不仅仅有列表和字符串,你在后面遇到的绝大多数数据结构,都是某种程度可迭代的,他们在被迭代时行为是通过各自独特的__iter__()方法来实现的,所以各不相同!

最后更新于