解包与列表推导式
前面的案例中出现过两段需要进一步解释的代码:
for index, name in enumerate(names, start=1):
print(index, name)
print(row_format.format(*student.values()))第一段为什么能同时得到 index 和 name?第二段中的 * 如何把一组数据交给函数?如果还要根据成绩生成新名单,怎样减少重复的循环代码?
本节围绕三个实际问题展开:从序列中取出多个值、把序列传给函数,以及根据已有数据生成新列表。
序列解包
当一条记录同时包含姓名和成绩时,我们希望一次取出多个字段,而不是逐个通过下标访问。解包的英文是 unpacking,可以理解为把容器拆开,再把其中的元素交给多个变量。
固定位置与忽略值
固定位置: 元组和列表都可以解包:
# 元组解包
name, chinese, math = ("张三", 88, 92)
print(name, chinese, math)
# 列表解包
name, chinese, math = ["李四", 85, 90]
print(name, chinese, math)运行结果:
张三 88 92
李四 85 90不使用 * 时,左边变量的数量要和右边元素的数量一致。数量对不上时,程序会出现 ValueError。
name, chinese = ("张三", 88, 92) # ValueError忽略值: 如果某个值暂时用不到,可以用 _ 接收:
name, _, math = ("张三", 88, 92)
print(name, math)运行结果:
张三 92_ 本质上也是变量,只是按照 Python 的常用习惯,它表示“这个值不打算使用”。
剩余元素接收
如果记录中的元素数量不固定,或只关心首尾数据,可以在一个变量前加 *,让它接收剩余元素:
first, *middle, last = [60, 70, 80, 90, 100]
print(first)
print(middle)
print(last)运行结果:
60
[70, 80, 90]
100带 * 的变量会得到一个列表。一次解包中只能出现一个这样的变量,否则 Python 无法判断怎样分配元素。
循环中的记录拆分
遍历名单时,enumerate() 每次会产生一个包含“序号和元素”的元组。可以先观察这个元组:
names = ["张三", "李四", "王五"]
for item in enumerate(names, start=1):
print(item)运行结果:
(1, '张三')
(2, '李四')
(3, '王五')可以直接在 for 后面解包这个元组:
names = ["张三", "李四", "王五"]
for index, name in enumerate(names, start=1):
print(f"{index}. {name}")运行结果:
1. 张三
2. 李四
3. 王五每次循环时,元组中的第一个值交给 index,第二个值交给 name。
函数调用的序列解包
当函数需要多个位置参数,而数据已经保存在列表或其他可遍历对象中时,可以在函数调用中使用 *,把序列拆成多个位置参数:
numbers = [1, 2, 3, 4, 5]
print(*numbers)运行结果:
1 2 3 4 5它的效果相当于:
print(1, 2, 3, 4, 5)学生信息管理系统中的表格输出也是同样的道理:
student = {
"name": "张三",
"chinese": 88,
"math": 92,
"english": 85,
"total": 265,
}
row_format = "{}\t{}\t{}\t{}\t{}"
print(row_format.format(*student.values()))运行结果:
张三 88 92 85 265student.values() 产生五个值,* 把它们逐个传给 format(),正好填入五个 {}。
列表推导式
如果要把一组成绩逐项转换成新成绩,或只保留达标成绩,可以根据已有数据生成一个新列表。列表推导式就是为这种场景准备的简洁写法。
逐项转换: 先看一个普通循环。给每个成绩增加 5 分,并把结果保存到新列表:
scores = [60, 72, 85, 90]
new_scores = []
for score in scores:
new_scores.append(score + 5)
print(new_scores)运行结果:
[65, 77, 90, 95]使用列表推导式,可以把创建列表的过程写在一行:
scores = [60, 72, 85, 90]
new_scores = [score + 5 for score in scores]
print(new_scores)结果仍然是:
[65, 77, 90, 95]基本结构如下:
[新元素 for 元素 in 可遍历对象]虽然“新元素”写在最前面,理解时可以先看后面的循环:
for score in scores:逐个取出原成绩score + 5:生成要放进新列表的值
增加筛选: 如果只保留符合条件的数据,可以在列表推导式后面增加 if:
scores = [45, 60, 72, 58, 90]
passed_scores = [score for score in scores if score >= 60]
print(passed_scores)运行结果:
[60, 72, 90]带筛选条件的结构如下:
[新元素 for 元素 in 可遍历对象 if 条件]阅读时按照这个顺序理解:
for:遍历原数据if:保留符合条件的数据- 最前面的表达式:生成新列表中的元素
案例:筛选达标学生
需求:从学生列表中找出总分不低于 180 分的学生,只把姓名放进新列表。
功能分析:先用 if 筛选达标学生,再用 student["name"] 提取姓名。
students = [
{"name": "张三", "total": 195},
{"name": "李四", "total": 175},
{"name": "王五", "total": 230},
]
passed_names = [
student["name"]
for student in students
if student["total"] >= 180
]
print(passed_names)运行结果:
['张三', '王五']这段推导式同时做了两件事:
- 用
if筛选总分达标的学生 - 用
student["name"]取出这些学生的姓名
代码较长时,可以像示例一样分成多行,不必强行挤在一行。
列表推导式的适用边界
列表推导式适合处理转换或筛选条件都比较简单的场景:
- 根据一个可遍历对象生成新列表
- 对每个元素做简单转换
- 使用一个简单条件筛选元素
如果包含多层循环、多个复杂条件或大量处理步骤,普通 for 循环通常更清楚。
小结
本节围绕四个问题组织了两组写法:
- 如何把序列中的多个元素交给变量?使用序列解包。
- 如何忽略暂时不用的值,或接收数量不固定的剩余元素?使用
_和*。 - 如何把一个序列传给需要多个位置参数的函数?在函数调用中使用
*解包。 - 如何根据已有数据生成新列表?使用列表推导式完成转换和筛选。
练习
- 把元组
("张三", 88, 92)解包到三个变量并打印 - 使用
first, *middle, last解包一个包含五个元素的列表 - 使用列表推导式生成
1到20之间所有奇数组成的列表 - 从学生列表中筛选总分不低于
200分的学生姓名