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

  1. import random
  2. f1 = 0
  3. f2 = 0
  4. f3 = 0
  5. f4 = 0
  6. f5 = 0
  7. f6 = 0
  8. for _ in range(6000):
  9. face = random.randint(1, 6)
  10. if face == 1:
  11. f1 += 1
  12. if face == 2:
  13. f2 += 1
  14. if face == 3:
  15. f3 += 1
  16. if face == 4:
  17. f4 += 1
  18. if face == 5:
  19. f5 += 1
  20. if face == 6:
  21. f6 += 1
  22. print(f'1点出现了{f1}次')
  23. print(f'1点出现了{f2}次')
  24. print(f'1点出现了{f3}次')
  25. print(f'1点出现了{f4}次')
  26. print(f'1点出现了{f5}次')
  27. print(f'1点出现了{f6}次')

看看上面的代码,相信大家一定觉得它非常的“笨重”和“丑陋”,更可怕的是,如果要统计掷2颗或者更多的色子统计每个点数出现的次数,那就需要定义更多的变量,写更多的分支结构。讲到这里,相信大家一定想问:有没有办法用一个变量来保存多个数据,有没有办法用统一的代码对多个数据进行操作?答案是肯定的,在Python中我们可以通过容器类型的变量来保存和操作多个数据,我们首先为大家介绍列表(list)这种新的数据类型。

定义和使用列表

在Python中,列表是由一系元素按特定顺序构成的数据序列,这样就意味着定义一个列表类型的变量,可以保存多个数据,而且允许有重复的数据。跟上一课我们讲到的字符串类型一样,列表也是一种结构化的、非标量类型,操作一个列表类型的变量,除了可以使用运算符还可以使用它的方法。

在Python中,可以使用[]字面量语法来定义列表,列表中的多个元素用逗号进行分隔,代码如下所示。

  1. items1 = [35, 12, 99, 68, 55, 87]
  2. print(items1)
  3. items2 = ['Python', 'Java', 'Go', 'Kotlin']
  4. print(items2)

除此以外,还可以通过Python内置的list函数将其他序列变成列表。准确的说,list并不是一个函数,而是创建列表对象的构造器(后面会讲到对象和构造器这两个概念)。

  1. items1 = list(range(1, 10))
  2. print(items1) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
  3. items2 = list('hello')
  4. print(items2) # ['h', 'e', 'l', 'l', 'o']

需要说明的是,列表是一种可变数据类型,也就是说列表可以添加元素、删除元素、更新元素,这一点跟我们上一课讲到的字符串有着鲜明的差别。字符串是一种不可变数据类型,也就是说对字符串做拼接、重复、转换大小写、修剪空格等操作的时候会产生新的字符串,原来的字符串并没有发生任何改变。

列表的运算符

和字符串类型一样,列表也支持拼接、重复、成员运算、索引和切片以及比较运算,对此我们不再进行赘述,请大家参考下面的代码。

  1. items1 = [35, 12, 99, 68, 55, 87]
  2. items2 = [45, 8, 29]
  3. # 列表的拼接
  4. items3 = items1 + items2
  5. print(items3) # [35, 12, 99, 68, 55, 87, 45, 8, 29]
  6. # 列表的重复
  7. items4 = ['hello'] * 3
  8. print(items4) # ['hello', 'hello', 'hello']
  9. # 列表的成员运算
  10. print(100 in items3) # False
  11. print('hello' in items4) # True
  12. # 获取列表的长度(元素个数)
  13. size = len(items3)
  14. print(size) # 9
  15. # 列表的索引
  16. print(items3[0], items3[-size]) # 35 35
  17. items3[-1] = 100
  18. print(items3[size - 1], items3[-1]) # 100 100
  19. # 列表的切片
  20. print(items3[:5]) # [35, 12, 99, 68, 55]
  21. print(items3[4:]) # [55, 87, 45, 8, 100]
  22. print(items3[-5:-7:-1]) # [55, 68]
  23. print(items3[::-2]) # [100, 45, 55, 99, 35]
  24. # 列表的比较运算
  25. items5 = [1, 2, 3, 4]
  26. items6 = list(range(1, 5))
  27. # 两个列表比较相等性比的是对应索引位置上的元素是否相等
  28. print(items5 == items6) # True
  29. items7 = [3, 2, 1]
  30. # 两个列表比较大小比的是对应索引位置上的元素的大小
  31. print(items5 <= items7) # True

值得一提的是,由于列表是可变类型,所以通过索引操作既可以获取列表中的元素,也可以更新列表中的元素。对列表做索引操作一样要注意索引越界的问题,对于有N个元素的列表,正向索引的范围是0N-1,负向索引的范围是-1-N,如果超出这个范围,将引发IndexError异常,错误信息为:list index out of range

列表元素的遍历

如果想逐个取出列表中的元素,可以使用for循环的,有以下两种做法。
方法一:

  1. items = ['Python', 'Java', 'Go', 'Kotlin']
  2. for index in range(len(items)):
  3. print(items[index])

方法二:

  1. items = ['Python', 'Java', 'Go', 'Kotlin']
  2. for item in items:
  3. print(item)

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

  1. import random
  2. counters = [0] * 6 # 列表中的六个元素分别表示1到6的点数出现的次数,初始都是0
  3. for _ in range(6000):
  4. face = random.randint(1, 6)
  5. counters[face-1] += 1 # 如果摇出1点counters[0]的值加1,如果摇出2点counters[1]的值加1,以此类推。
  6. for face in range(1, 7):
  7. print(f'{face}点出现了{counters[face-1]}次')

列表的方法

和字符串一样,列表类型的方法也很多,下面为大家讲解比较重要的方法。

添加和删除元素

  1. items = ['Python', 'Java', 'Go', 'Kotlin']
  2. # 使用append方法在列表尾部添加元素
  3. items.append('Swift')
  4. print(items) # ['Python', 'Java', 'Go', 'Kotlin', 'Swift']
  5. # 使用insert方法在列表指定索引位置插入元素
  6. items.insert(2, 'SQL')
  7. print(items) # ['Python', 'Java', 'SQL', 'Go', 'Kotlin', 'Swift']
  8. # 删除指定的元素
  9. items.remove('Java')
  10. print(items) # ['Python', 'SQL', 'Go', 'Kotlin', 'Swift']
  11. # 删除指定索引位置的元素
  12. items.pop(0)
  13. items.pop(len(items) - 1)
  14. print(items) # ['SQL', 'Go', 'Kotlin']
  15. # 清空列表中的元素
  16. items.clear()
  17. print(items) # []

注意:在使用remove方法删除元素时,如果要删除的元素并不在列表中,会引发ValueError异常,错误消息是:list.remove(x): x not in list。在使用pop方法删除元素时,如果索引的值超出了范围,会引发IndexError异常,错误消息是:pop index out of range

从列表中删除元素其实还有一种方式,就是使用Python中的del关键字后面跟要删除的元素,这种做法跟使用pop方法指定索引删除元素没有实质性的区别,但pop会返回删除的元素,del在性能上略优(del对应字节码指令是DELETE_SUBSCR,而pop对应的字节码指令是CALL_METHODPOP_TOP)。

  1. items = ['Python', 'Java', 'Go', 'Kotlin']
  2. print(items.pop(0)) # Python
  3. del items[1]
  4. print(items) # ['Java', 'Kotlin']

元素位置和次数

列表类型的index方法可以查找某个元素在列表中的索引位置;因为列表中允许有重复的元素,所以列表类型提供了count方法来统计一个元素在列表中出现的次数。请看下面的代码。

  1. items = ['Python', 'Java', 'Java', 'Go', 'Kotlin', 'Python']
  2. # 查找元素的索引位置
  3. print(items.index('Python')) # 0
  4. print(items.index('Python', 2)) # 5
  5. # 注意:虽然列表中有'Java',但是从索引为3这个位置开始后面是没有'Java'的
  6. print(items.index('Java', 3)) # ValueError: 'Java' is not in list
  7. # 查找元素出现的次数
  8. print(items.count('Python')) # 2
  9. print(items.count('Go')) # 1
  10. print(items.count('Swfit')) # 0

元素排序和反转

列表的sort操作可以实现列表元素的排序,而reverse操作可以实现元素的反转,代码如下所示。

  1. items = ['Python', 'Java', 'Go', 'Kotlin', 'Python']
  2. # 排序
  3. items.sort()
  4. print(items) # ['Go', 'Java', 'Kotlin', 'Python', 'Python']
  5. # 反转
  6. items.reverse()
  7. print(items) # ['Python', 'Python', 'Kotlin', 'Java', 'Go']

列表的生成式

在Python中,列表还可以通过一种特殊的自变量语法来创建,这种语法叫做生成式。我们给出两段代码,大家可以做一个对比,看看哪一种方式更加简单优雅。
通过for循环为空列表添加元素。

  1. # 创建一个由1到9的数字构成的列表
  2. items1 = []
  3. for x in range(1, 10):
  4. items1.append(x)
  5. print(items1)
  6. # 创建一个由'hello world'中除空格和元音字母外的字符构成的列表
  7. items2 = []
  8. for x in 'hello world':
  9. if x not in ' aeiou':
  10. items2.append(x)
  11. print(items2)
  12. # 创建一个由个两个字符串中字符的笛卡尔积构成的列表
  13. items3 = []
  14. for x in 'ABC':
  15. for y in '12':
  16. items3.append(x + y)
  17. print(items3)

通过生成式创建列表。

  1. # 创建一个由1到9的数字构成的列表
  2. items1 = [x for x in range(1, 10)]
  3. print(items1) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
  4. # 创建一个由'hello world'中除空格和元音字母外的字符构成的列表
  5. items2 = [x for x in 'hello world' if x not in ' aeiou']
  6. print(items2) # ['h', 'l', 'l', 'w', 'r', 'l', 'd']
  7. # 创建一个由个两个字符串中字符的笛卡尔积构成的列表
  8. items3 = [x + y for x in 'ABC' for y in '12']
  9. print(items3) # ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']

下面这种方式不仅代码简单优雅,而且性能也优于上面使用for循环和append方法向空列表中追加元素的方式。可以简单跟大家交待下为什么生成式拥有更好的性能,那是因为Python解释器的字节码指令中有专门针对生成式的指令(LIST_APPEND指令);而for循环是通过方法调用(LOAD_METHODCALL_METHOD指令)的方式为列表添加元素,方法调用本身就是一个相对耗时的操作。对这一点不理解也没有关系,记住“强烈建议用生成式语法来创建列表”这个结论就可以了。

嵌套的列表

Python语言没有限定列表中的元素必须是相同的数据类型,也就是说一个列表中的元素可以任意的数据类型,当然也包括列表。如果列表中的元素又是列表,那么我们可以称之为嵌套的列表。嵌套的列表可以用来表示表格或数学上的矩阵,例如:我们想保存5个学生3门课程的成绩,可以定义一个保存5个元素的列表保存5个学生的信息,而每个列表元素又是3个元素构成的列表,分别代表3门课程的成绩。但是,一定要注意下面的代码是有问题的。

  1. scores = [[0] * 3] * 5
  2. print(scores) # [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

看上去我们好像创建了一个5 * 3的嵌套列表,但实际上当我们录入第一个学生的第一门成绩后,你就会发现问题来了,我们看看下面代码的输出。

  1. # 嵌套的列表需要多次索引操作才能获取元素
  2. scores[0][0] = 95
  3. print(scores) # [[95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0]]

我们不去过多的解释为什么会出现这样的问题,如果想深入研究这个问题,可以通过Python Tutor网站的可视化代码执行功能,看看创建列表时计算机内存中发生了怎样的变化,下面的图就是在这个网站上生成的。建议大家不去纠结这个问题,现阶段只需要记住不能用[[0] * 3] * 5]这种方式来创建嵌套列表就行了。
image.png
那么创建嵌套列表的正确做法是什么呢,下面的代码会给你答案。

  1. scores = [[0] * 3 for _ in range(5)]
  2. scores[0][0] = 95
  3. print(scores)
  4. # [[95, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

image.png