## 常用数据结构之列表-1

在开始本节课的内容之前，我们先给大家一个编程任务，将一颗色子掷 6000 次，统计每种点数出现的次数。这个任务对大家来说应该是非常简单的，我们可以用 1 到 6 均匀分布的随机数来模拟掷色子，然后用 6 个变量分别记录每个点数出现的次数，相信通过前面的学习，大家都能比较顺利的写出下面的代码。

```python
"""
将一颗色子掷6000次，统计每种点数出现的次数

Author: 骆昊
Version: 1.0
"""
import random

f1 = 0
f2 = 0
f3 = 0
f4 = 0
f5 = 0
f6 = 0
for _ in range(6000):
    face = random.randrange(1, 7)
    if face == 1:
        f1 += 1
    elif face == 2:
        f2 += 1
    elif face == 3:
        f3 += 1
    elif face == 4:
        f4 += 1
    elif face == 5:
        f5 += 1
    else:
        f6 += 1
print(f'1点出现了{f1}次')
print(f'2点出现了{f2}次')
print(f'3点出现了{f3}次')
print(f'4点出现了{f4}次')
print(f'5点出现了{f5}次')
print(f'6点出现了{f6}次')
```

上面的代码非常有多么“丑陋”相信就不用我多说了。当然，更为可怕的是，如果我们要掷两颗或者掷更多的色子，然后统计每种点数出现的次数，那就需要定义更多的变量，写更多的分支结构，大家想想都会感到恶心。讲到这里，相信大家心中已经有一个疑问了：有没有办法用一个变量来保存多个数据，有没有办法用统一的代码对多个数据进行操作？答案是肯定的，在 Python 语言中我们可以通过容器型变量来保存和操作多个数据，我们首先为大家介绍列表（`list`）这种新的数据类型。

### 创建列表

在 Python 中，**列表是由一系列元素按特定顺序构成的数据序列**，这就意味着如果我们定义一个列表类型的变量，**可以用它来保存多个数据**。在 Python 中，可以使用`[]`字面量语法来定义列表，列表中的多个元素用逗号进行分隔，代码如下所示。

```python
items1 = [35, 12, 99, 68, 55, 35, 87]
items2 = ['Python', 'Java', 'Go', 'Kotlin']
items3 = [100, 12.3, 'Python', True]
print(items1)  # [35, 12, 99, 68, 55, 35, 87]
print(items2)  # ['Python', 'Java', 'Go', 'Kotlin']
print(items3)  # [100, 12.3, 'Python', True]
```

> **说明**：列表中可以有重复元素，例如`items1`中的`35`；列表中可以有不同类型的元素，例如`items3`中有`int`类型、`float`类型、`str`类型和`bool`类型的元素，但是我们通常并不建议将不同类型的元素放在同一个列表中，主要是操作起来极为不便。

我们可以使用`type`函数来查看变量的类型，有兴趣的小伙伴可以自行查看上面的变量`items1`到底是什么类型。因为列表可以保存多个元素，它是一种容器型的数据类型，所以我们在给列表类型的变量起名字时，变量名通常用复数形式的单词。

除此以外，还可以通过 Python 内置的`list`函数将其他序列变成列表。准确的说，`list`并不是一个普通的函数，它是创建列表对象的构造器，后面的课程会为大家介绍对象和构造器这些概念。

```python
items4 = list(range(1, 10))
items5 = list('hello')
print(items4)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(items5)  # ['h', 'e', 'l', 'l', 'o']
```

> **说明**：`range(1, 10)`会产生`1`到`9`的整数序列，给到`list`构造器中，会创建出由`1`到`9`的整数构成的列表。字符串是字符构成的序列，上面的`list('hello')`用字符串`hello`的字符作为列表元素，创建了列表对象。

### 列表的运算

我们可以使用`+`运算符实现两个列表的拼接，拼接运算会将两个列表中的元素连接起来放到一个列表中，代码如下所示。

```python
items5 = [35, 12, 99, 45, 66]
items6 = [45, 58, 29]
items7 = ['Python', 'Java', 'JavaScript']
print(items5 + items6)  # [35, 12, 99, 45, 66, 45, 58, 29]
print(items6 + items7)  # [45, 58, 29, 'Python', 'Java', 'JavaScript']
items5 += items6
print(items5)  # [35, 12, 99, 45, 66, 45, 58, 29]
```

我们可以使用`*`运算符实现列表的重复运算，`*`运算符会将列表元素重复指定的次数，我们在上面的代码中增加两行，如下所示。

```python
print(items6 * 3)  # [45, 58, 29, 45, 58, 29, 45, 58, 29]
print(items7 * 2)  # ['Python', 'Java', 'JavaScript', 'Python', 'Java', 'JavaScript']
```

我们可以使用`in`或`not in`运算符判断一个元素在不在列表中，我们在上面的代码代码中再增加两行，如下所示。

```python
print(29 in items6)  # True
print(99 in items6)  # False
print('C++' not in items7)     # True
print('Python' not in items7)  # False
```

由于列表中有多个元素，而且元素是按照特定顺序放在列表中的，所以当我们想操作列表中的某个元素时，可以使用`[]`运算符，通过在`[]`中指定元素的位置来访问该元素，这种运算称为索引运算。需要说明的是，`[]`的元素位置可以是`0`到`N - 1`的整数，也可以是`-1`到`-N`的整数，分别称为正向索引和反向索引，其中`N`代表列表元素的个数。对于正向索引，`[0]`可以访问列表中的第一个元素，`[N - 1]`可以访问最后一个元素；对于反向索引，`[-1]`可以访问列表中的最后一个元素，`[-N]`可以访问第一个元素，代码如下所示。

```python
items8 = ['apple', 'waxberry', 'pitaya', 'peach', 'watermelon']
print(items8[0])   # apple
print(items8[2])   # pitaya
print(items8[4])   # watermelon
items8[2] = 'durian'
print(items8)      # ['apple', 'waxberry', 'durian', 'peach', 'watermelon']
print(items8[-5])  # 'apple'
print(items8[-4])  # 'waxberry'
print(items8[-1])  # watermelon
items8[-4] = 'strawberry'
print(items8)      # ['apple', 'strawberry', 'durian', 'peach', 'watermelon']
```

在使用索引运算的时候要避免出现索引越界的情况，对于上面的`items8`，如果我们访问`items8[5]`或`items8[-6]`，就会引发`IndexError`错误，导致程序崩溃，对应的错误信息是：*list index out of range*，翻译成中文就是“数组索引超出范围”。因为对于只有五个元素的列表`items8`，有效的正向索引是`0`到`4`，有效的反向索引是`-1`到`-5`。

如果希望一次性访问列表中的多个元素，我们可以使用切片运算。切片运算是形如`[start:end:stride]`的运算符，其中`start`代表访问列表元素的起始位置，`end`代表访问列表元素的终止位置（终止位置的元素无法访问），而`stride`则代表了跨度，简单的说就是位置的增量，比如我们访问的第一个元素在`start`位置，那么第二个元素就在`start + stride`位置，当然`start + stride`要小于`end`。我们给上面的代码增加下面的语句，来使用切片运算符访问列表元素。

```python
print(items8[1:3:1])     # ['strawberry', 'durian']
print(items8[0:3:1])     # ['apple', 'strawberry', 'durian']
print(items8[0:5:2])     # ['apple', 'durian', 'watermelon']
print(items8[-4:-2:1])   # ['strawberry', 'durian']
print(items8[-2:-6:-1])  # ['peach', 'durian', 'strawberry', 'apple']
```

> **提醒**：大家可以看看上面代码中的最后一行，想一想当跨度为负数时，切片运算是如何访问元素的。

如果`start`值等于`0`，那么在使用切片运算符时可以将其省略；如果`end`值等于`N`，`N`代表列表元素的个数，那么在使用切片运算符时可以将其省略；如果`stride`值等于`1`，那么在使用切片运算符时也可以将其省略。所以，下面的代码跟上面的代码作用完全相同。

```python
print(items8[1:3])     # ['strawberry', 'durian']
print(items8[:3:1])    # ['apple', 'strawberry', 'durian']
print(items8[::2])     # ['apple', 'durian', 'watermelon']
print(items8[-4:-2])   # ['strawberry', 'durian']
print(items8[-2::-1])  # ['peach', 'durian', 'strawberry', 'apple']
```

事实上，我们还可以通过切片操作修改列表中的元素，例如我们给上面的代码再加上一行，大家可以看看这里的输出。

```python
items8[1:3] = ['x', 'o']
print(items8)  # ['apple', 'x', 'o', 'peach', 'watermelon']
```

两个列表还可以做关系运算，我们可以比较两个列表是否相等，也可以给两个列表比大小，代码如下所示。

```python
nums1 = [1, 2, 3, 4]
nums2 = list(range(1, 5))
nums3 = [3, 2, 1]
print(nums1 == nums2)  # True
print(nums1 != nums2)  # False
print(nums1 <= nums3)  # True
print(nums2 >= nums3)  # False
```

> **说明**：上面的`nums1`和`nums2`对应元素完全相同，所以`==`运算的结果是`True`。`nums2`和`nums3`的比较，由于`nums2`的第一个元素`1`小于`nums3`的第一个元素`3`，所以`nums2 >= nums3`比较的结果是`False`。两个列表的关系运算在实际工作并不那么常用，如果实在不理解就跳过吧，不用纠结。

### 元素的遍历

如果想逐个取出列表中的元素，可以使用`for-in`循环的，有以下两种做法。

方法一：在循环结构中通过索引运算，遍历列表元素。

```python
languages = ['Python', 'Java', 'C++', 'Kotlin']
for index in range(len(languages)):
    print(languages[index])
```

输出：

```
Python
Java
C++
Kotlin
```

> **说明**：上面的`len`函数可以获取列表元素的个数`N`，而`range(N)`则构成了从`0`到`N-1`的范围，刚好可以作为列表元素的索引。

方法二：直接对列表做循环，循环变量就是列表元素的代表。

```python
languages = ['Python', 'Java', 'C++', 'Kotlin']
for language in languages:
    print(language)
```

输出：

```
Python
Java
C++
Kotlin
```

### 总结

讲到这里，我们可以用列表的知识来重构上面“掷色子统计每种点数出现次数”的代码。

```python
"""
将一颗色子掷6000次，统计每种点数出现的次数

Author: 骆昊
Version: 1.1
"""
import random

counters = [0] * 6
# 模拟掷色子记录每种点数出现的次数
for _ in range(6000):
    face = random.randrange(1, 7)
    counters[face - 1] += 1
# 输出每种点数出现的次数
for face in range(1, 7):
    print(f'{face}点出现了{counters[face - 1]}次')
```

上面的代码中，我们用`counters`列表中的六个元素分别表示 1 到 6 点出现的次数，最开始的时候六个元素的值都是 0。接下来，我们用 1 到 6 均匀分布的随机数模拟掷色子，如果摇出 1 点，`counters[0]`的值加 1，如果摇出 2 点，`counters[1]`的值加 1，以此类推。大家感受一下，由于使用了列表类型加上循环结构，我们对数据的处理是批量性的，这就使得修改后的代码比之前的代码要简单优雅得多。
