Python 常用数据结构:列表操作与生成式详解
列表的方法
列表类型的变量拥有很多方法可以帮助我们操作一个列表。假设我们有名为 foos 的列表,列表有名为 bar 的方法,那么使用列表方法的语法是:foos.bar()。这是一种通过对象引用调用对象方法的语法,也称为给对象发消息。
添加和删除元素
列表是一种可变容器,我们可以向容器中添加元素、从容器移除元素,也可以修改现有容器中的元素。我们可以使用列表的 append 方法向列表中追加元素,使用 insert 方法向列表中插入元素。追加指的是将元素添加到列表的末尾,而插入则是在指定的位置添加新元素。
languages = ['Python', 'Java', 'C++']
languages.append('JavaScript')
print(languages)
languages.insert(1, 'SQL')
print(languages)
我们可以用列表的 remove 方法从列表中删除指定元素。需要注意的是,如果要删除的元素并不在列表中,会引发 ValueError 错误导致程序崩溃,所以建议大家在删除元素时,先用成员运算做一个判断。我们还可以使用 pop 方法从列表中删除元素,pop 方法默认删除列表中的最后一个元素,当然也可以给一个位置,删除指定位置的元素。在使用 pop 方法删除元素时,如果索引的值超出了范围,会引发 IndexError 异常,导致程序崩溃。除此之外,列表还有一个 clear 方法,可以清空列表中的元素。
languages = ['Python', 'SQL', 'Java', 'C++', 'JavaScript']
if 'Java' in languages:
languages.remove('Java')
if 'Swift' in languages:
languages.remove('Swift')
print(languages)
languages.pop()
temp = languages.pop(1)
print(temp)
languages.append(temp)
print(languages)
languages.clear()
print(languages)
说明:pop 方法删除元素时会得到被删除的元素,上面的代码中,我们将 pop 方法删除的元素赋值给了名为 temp 的变量。当然如果你愿意,还可以把这个元素再次加入到列表中,正如上面的代码 languages.append(temp) 所做的那样。
这里还有一个小问题,例如 languages 列表中有多个 'Python',那么我们用 languages.remove('Python') 是删除所有的 'Python',还是删除第一个 'Python'?大家可以先猜一猜,然后再自己动手尝试一下。实际上,remove 只删除第一个匹配的元素。
从列表中删除元素其实还有一种方式,就是使用 Python 中的 del 关键字后面跟要删除的元素,这种做法跟使用 pop 方法指定索引删除元素没有实质性的区别,但后者会返回删除的元素,前者在性能上略优,因为 del 对应的底层字节码指令是 DELETE_SUBSCR,而 pop 对应的底层字节码指令是 CALL_METHOD 和 POP_TOP。
items = ['Python', 'Java', 'C++']
del items[1]
print(items)
元素位置和频次
列表的 index 方法可以查找某个元素在列表中的索引位置,如果找不到指定的元素,index 方法会引发 ValueError 错误;列表的 count 方法可以统计一个元素在列表中出现的次数。
items = ['Python', 'Java', 'Java', 'C++', 'Kotlin', 'Python']
print(items.index('Python'))
print(items.index('Python', 1))
print(items.count('Python'))
print(items.count('Kotlin'))
print(items.count('Swift'))
print(items.index('Java', 3))
元素排序和反转
列表的 sort 操作可以实现列表元素的排序,而 reverse 操作可以实现元素的反转。
items = ['Python', 'Java', 'C++', 'Kotlin', 'Swift']
items.sort()
print(items)
items.reverse()
print(items)
列表生成式
在 Python 中,列表还可以通过一种特殊的字面量语法来创建,这种语法叫做生成式。下面,我们通过例子来说明使用列表生成式创建列表到底有什么好处。
场景一:筛选数字
创建一个取值范围在 1 到 99 且能被 3 或者 5 整除的数字构成的列表。
items = []
for i in range(1, 100):
if i % 3 == 0 or i % 5 == 0:
items.append(i)
print(items)
items = [i for i in range(1, 100) if i % 3 == 0 or i % 5 == 0]
print(items)
场景二:元素变换
有一个整数列表 nums1,创建一个新的列表 nums2,nums2 中的元素是 nums1 中对应元素的平方。
nums1 = [35, 12, 97, 64, 55]
nums2 = [num ** 2 for num in nums1]
print(nums2)
场景三:条件过滤
有一个整数列表 nums1,创建一个新的列表 nums2,将 nums1 中大于 50 的元素放到 nums2 中。
nums1 = [35, 12, 97, 64, 55]
nums2 = [num for num in nums1 if num > 50]
print(nums2)
使用列表生成式创建列表不仅代码简单优雅,而且性能上也优于使用 for-in 循环和 append 方法向空列表中追加元素的方式。这是因为 Python 解释器的字节码指令中有专门针对生成式的指令(LIST_APPEND 指令);而 for 循环是通过方法调用的方式为列表添加元素,方法调用本身就是一个相对比较耗时的操作。
嵌套列表
Python 语言没有限定列表中的元素必须是相同的数据类型,也就是说一个列表中的元素可以是任意的数据类型,当然也包括列表本身。如果列表中的元素也是列表,那么我们可以称之为嵌套的列表。嵌套的列表可以用来表示表格或数学上的矩阵。
例如:我们想保存 5 个学生 3 门课程的成绩,可以用如下所示的列表。
scores = [[95, 83, 92], [80, 75, 82], [92, 97, 90], [80, 78, 69], [65, 66, 89]]
print(scores[0])
print(scores[0][1])
对于上面的嵌套列表,每个元素相当于就是一个学生 3 门课程的成绩,例如 [95, 83, 92],而这个列表中的 83 代表了这个学生某一门课的成绩。如果想访问这个值,可以使用两次索引运算 scores[0][1],其中 scores[0] 可以得到 [95, 83, 92] 这个列表,再次使用索引运算 [1] 就可以获得该列表中的第二个元素。
嵌套列表的初始化
如果想通过键盘输入的方式来录入 5 个学生 3 门课程的成绩并保存在列表中,可以使用如下所示的代码。
scores = []
for _ in range(5):
temp = []
for _ in range(3):
score = int(input('请输入成绩:'))
temp.append(score)
scores.append(temp)
print(scores)
如果想通过产生随机数的方式来生成 5 个学生 3 门课程的成绩并保存在列表中,我们可以使用列表生成式。
import random
scores = [[random.randrange(60, 101) for _ in range(3)] for _ in range(5)]
print(scores)
注意:上面的代码 [random.randrange(60, 101) for _ in range(3)] 可以产生由 3 个随机整数构成的列表,我们把这段代码又放在了另一个列表生成式中作为列表的元素,这样的元素一共生成 5 个,最终得到了一个嵌套列表。
嵌套列表的陷阱
在处理嵌套列表时,需要特别注意浅拷贝的问题。如果使用 * 运算符或切片复制外层列表,内层列表仍然是引用同一个对象。这可能导致修改其中一个子列表时,意外影响其他子列表。
wrong_scores = [[]] * 5
wrong_scores[0].append(95)
print(wrong_scores)
right_scores = [[] for _ in range(5)]
right_scores[0].append(95)
print(right_scores)
列表应用举例
下面我们通过一个双色球随机选号的例子为大家讲解列表的应用。双色球是由中国福利彩票发行管理中心发售的乐透型彩票,每注投注号码由 6 个红色球和 1 个蓝色球组成。红色球号码从 1 到 33 中选择,蓝色球号码从 1 到 16 中选择。每注需要选择 6 个红色球号码和 1 个蓝色球号码。
下面,我们通过 Python 程序来生成一组随机号码。
"""
双色球随机选号程序
Version: 1.0
"""
import random
red_balls = list(range(1, 34))
selected_balls = []
for _ in range(6):
index = random.randrange(len(red_balls))
selected_balls.append(red_balls.pop(index))
selected_balls.sort()
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
blue_ball = random.randrange(1, 17)
print(f'\033[034m{blue_ball:0>2d}\033[0m')
说明:上面代码中 print(f'\033[0m...\033[0m') 是为了控制输出内容的颜色,红色球输出成红色,蓝色球输出成蓝色。其中省略号代表我们要输出的内容,\033[0m 是一个控制码,表示关闭所有属性。
我们还可以利用 random 模块提供的 sample 和 choice 函数来简化上面的代码,前者可以实现无放回随机抽样,后者可以实现随机抽取一个元素。
"""
双色球随机选号程序
Version: 1.1
"""
import random
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
selected_balls = random.sample(red_balls, 6)
selected_balls.sort()
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
blue_ball = random.choice(blue_balls)
print(f'\033[034m{blue_ball:0>2d}\033[0m')
如果要实现随机生成 N 注号码,我们只需要将上面的代码放到一个 N 次的循环中。
"""
双色球随机选号程序
Version: 1.2
"""
import random
n = int(input('生成几注号码:'))
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
for _ in range(n):
selected_balls = random.sample(red_balls, 6)
selected_balls.sort()
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
blue_ball = random.choice(blue_balls)
print(f'\033[034m{blue_ball:0>2d}\033[0m')
总结与最佳实践
Python 中的列表底层是一个可以动态扩容的数组,列表元素在计算机内存中是连续存储的,所以可以实现随机访问(通过一个有效的索引获取对应的元素且操作时间与列表元素个数无关)。我们可以暂时不去触碰这些底层的存储细节,也不需要大家理解列表每个方法的渐近时间复杂度,大家先学会用列表解决工作中的问题,这一点更为重要。
在实际开发中,使用列表时建议遵循以下最佳实践:
- 优先使用列表生成式:相比传统的
for 循环加 append,列表生成式通常具有更好的可读性和执行效率。
- 注意可变对象的引用:在初始化嵌套列表时,避免使用
[[None]*n]*m 这种写法,应使用列表推导式确保每个子列表都是独立的对象。
- 合理使用内置方法:如
in 操作符用于成员检查比遍历更高效,extend 方法用于合并列表比多次 append 更简洁。
- 避免在遍历时修改列表:如果在遍历列表的同时修改其长度,可能会导致索引越界或跳过元素,建议使用列表生成式创建新列表代替原地修改。
掌握列表的这些特性和操作方法,是学习 Python 数据处理的基础。后续章节将介绍元组、字典等更多数据结构,它们共同构成了 Python 强大的数据操作能力。