Skip to content

解包与列表推导式 ​

前面的案例中出现过两段需要进一步解释的代码:

python
for index, name in enumerate(names, start=1):
    print(index, name)

print(row_format.format(*student.values()))

第一段为什么能同时得到 index 和 name?第二段中的 * 如何把一组数据交给函数?如果还要根据成绩生成新名单,怎样减少重复的循环代码?

本节围绕三个实际问题展开:从序列中取出多个值、把序列传给函数,以及根据已有数据生成新列表。

序列解包三大范式与槽位分配机制图

序列解包 ​

当一条记录同时包含姓名和成绩时,我们希望一次取出多个字段,而不是逐个通过下标访问。解包的英文是 unpacking,可以理解为把容器拆开,再把其中的元素交给多个变量。

固定位置与忽略值 ​

固定位置: 元组和列表都可以解包:

python
# 元组解包
name, chinese, math = ("张三", 88, 92)
print(name, chinese, math)

# 列表解包
name, chinese, math = ["李四", 85, 90]
print(name, chinese, math)

运行结果:

text
张三 88 92
李四 85 90

不使用 * 时,左边变量的数量要和右边元素的数量一致。数量对不上时,程序会出现 ValueError。

python
name, chinese = ("张三", 88, 92)  # ValueError

忽略值: 如果某个值暂时用不到,可以用 _ 接收:

python
name, _, math = ("张三", 88, 92)
print(name, math)

运行结果:

text
张三 92

_ 本质上也是变量,只是按照 Python 的常用习惯,它表示“这个值不打算使用”。

剩余元素接收 ​

如果记录中的元素数量不固定,或只关心首尾数据,可以在一个变量前加 *,让它接收剩余元素:

python
first, *middle, last = [60, 70, 80, 90, 100]

print(first)
print(middle)
print(last)

运行结果:

text
60
[70, 80, 90]
100

带 * 的变量会得到一个列表。一次解包中只能出现一个这样的变量,否则 Python 无法判断怎样分配元素。

循环中的记录拆分 ​

遍历名单时,enumerate() 每次会产生一个包含“序号和元素”的元组。可以先观察这个元组:

python
names = ["张三", "李四", "王五"]

for item in enumerate(names, start=1):
    print(item)

运行结果:

text
(1, '张三')
(2, '李四')
(3, '王五')

可以直接在 for 后面解包这个元组:

python
names = ["张三", "李四", "王五"]

for index, name in enumerate(names, start=1):
    print(f"{index}. {name}")

运行结果:

text
1. 张三
2. 李四
3. 王五

每次循环时,元组中的第一个值交给 index,第二个值交给 name。

for 循环元组自动拆箱解包流水线对比图

函数调用的序列解包 ​

当函数需要多个位置参数,而数据已经保存在列表或其他可遍历对象中时,可以在函数调用中使用 *,把序列拆成多个位置参数:

python
numbers = [1, 2, 3, 4, 5]
print(*numbers)

运行结果:

text
1 2 3 4 5

它的效果相当于:

python
print(1, 2, 3, 4, 5)

学生信息管理系统中的表格输出也是同样的道理:

python
student = {
    "name": "张三",
    "chinese": 88,
    "math": 92,
    "english": 85,
    "total": 265,
}
row_format = "{}\t{}\t{}\t{}\t{}"

print(row_format.format(*student.values()))

运行结果:

text
张三    88    92    85    265

student.values() 产生五个值,* 把它们逐个传给 format(),正好填入五个 {}。

函数调用中星号实参解包与占位符映射对比图

列表推导式 ​

如果要把一组成绩逐项转换成新成绩,或只保留达标成绩,可以根据已有数据生成一个新列表。列表推导式就是为这种场景准备的简洁写法。

逐项转换: 先看一个普通循环。给每个成绩增加 5 分,并把结果保存到新列表:

python
scores = [60, 72, 85, 90]
new_scores = []

for score in scores:
    new_scores.append(score + 5)

print(new_scores)

运行结果:

text
[65, 77, 90, 95]

使用列表推导式,可以把创建列表的过程写在一行:

python
scores = [60, 72, 85, 90]
new_scores = [score + 5 for score in scores]

print(new_scores)

结果仍然是:

text
[65, 77, 90, 95]

基本结构如下:

python
[新元素 for 元素 in 可遍历对象]

虽然“新元素”写在最前面,理解时可以先看后面的循环:

  1. for score in scores:逐个取出原成绩
  2. score + 5:生成要放进新列表的值

增加筛选: 如果只保留符合条件的数据,可以在列表推导式后面增加 if:

python
scores = [45, 60, 72, 58, 90]
passed_scores = [score for score in scores if score >= 60]

print(passed_scores)

运行结果:

text
[60, 72, 90]

带筛选条件的结构如下:

python
[新元素 for 元素 in 可遍历对象 if 条件]

阅读时按照这个顺序理解:

  1. for:遍历原数据
  2. if:保留符合条件的数据
  3. 最前面的表达式:生成新列表中的元素

列表推导式与传统 for 循环结构映射及执行动线图

案例:筛选达标学生 ​

需求:从学生列表中找出总分不低于 180 分的学生,只把姓名放进新列表。

功能分析:先用 if 筛选达标学生,再用 student["name"] 提取姓名。

python
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)

运行结果:

text
['张三', '王五']

这段推导式同时做了两件事:

  1. 用 if 筛选总分达标的学生
  2. 用 student["name"] 取出这些学生的姓名

代码较长时,可以像示例一样分成多行,不必强行挤在一行。

学生字典列表筛选与属性投影全景管道图

列表推导式的适用边界 ​

列表推导式适合处理转换或筛选条件都比较简单的场景:

  1. 根据一个可遍历对象生成新列表
  2. 对每个元素做简单转换
  3. 使用一个简单条件筛选元素

如果包含多层循环、多个复杂条件或大量处理步骤,普通 for 循环通常更清楚。

小结 ​

本节围绕四个问题组织了两组写法:

  1. 如何把序列中的多个元素交给变量?使用序列解包。
  2. 如何忽略暂时不用的值,或接收数量不固定的剩余元素?使用 _ 和 *。
  3. 如何把一个序列传给需要多个位置参数的函数?在函数调用中使用 * 解包。
  4. 如何根据已有数据生成新列表?使用列表推导式完成转换和筛选。

练习 ​

  1. 把元组 ("张三", 88, 92) 解包到三个变量并打印
  2. 使用 first, *middle, last 解包一个包含五个元素的列表
  3. 使用列表推导式生成 1 到 20 之间所有奇数组成的列表
  4. 从学生列表中筛选总分不低于 200 分的学生姓名