## 常用数据结构之列表-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,以此类推。大家感受一下,由于使用了列表类型加上循环结构,我们对数据的处理是批量性的,这就使得修改后的代码比之前的代码要简单优雅得多。