> For the complete documentation index, see [llms.txt](https://hq-1.gitbook.io/python/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hq-1.gitbook.io/python/di-si-zhang-cao-zuo-lie-biao-0511.md).

# 第四章 操作列表

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

```
# 列表切片
>>> 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 语言中类似的循环](https://stackoverflow.com/questions/1597830/iterate-through-a-c-array)，你就会发现，为什么我们说 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 对象操作的简写。**&#x6BD4;如包在切片语法外围的中括号，其实就是调用 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
```

{% hint style="info" %}
&#x20;小问题：既然 int 不可循环，那我们怎么实现用 for 循环 1 - 123 的每一个整数呢？
{% endhint %}

## 列表拆分

考虑下面这样一个需求：

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

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

## 介绍一下列表推导式和 `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 中获取更多他们的说明，以及精心设计的案例!&#x20;

* 列表拆分（unpack）&#x20;
* 星号表达式（starred expression）&#x20;
* 对象的内置方法是许多语法形式的本质（如`__iter__()`），内置方法也叫魔法方法（magic methods， python 的魔力来源！） ，这是个很大的话题，有一本叫做 fluent python 的进阶书籍对我帮助很大！但这本书不太适合刚刚接触 python 的新手。
* 列表推导式（list comprehension）

这些内容不仅仅是看上去简洁优美的奇技淫巧而已，在深入学习他们的过程中，我们也更能了解 python 的许多深刻特性（三大定理）。**这对于理解后面的很多内容都是大有裨益的**。

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