Python 基础语法(七):推导式与常见遍历写法

我们已经接触了 Python 中几种常用的容器。平时处理这些数据时,经常会遇到一类重复操作。遍历原来的数据,做一次计算或判断,再把结果放进一个新的列表、字典或者集合中。

思维导图

这些操作当然可以使用普通的 for 循环完成,不过代码比较简单时,Python 还提供了推导式。它可以把"遍历、处理、筛选、生成新容器"写在一行里。这一章就来讲推导式,以及和遍历经常一起使用的 range()enumerate()zip()sorted() 等写法。

一、列表推导式

先看一段普通的循环。下面的代码把列表中的每个数字乘以 2,再放进一个新列表:

python 复制代码
numbers = [1, 2, 3, 4, 5]
doubled = []

for number in numbers:
    doubled.append(number * 2)

print(doubled)

输出结果为:

复制代码
[2, 4, 6, 8, 10]

改成列表推导式以后,可以写成:

python 复制代码
numbers = [1, 2, 3, 4, 5]
doubled = [number * 2 for number in numbers]

print(doubled)

它的基本结构是:

python 复制代码
[表达式 for 变量 in 可迭代对象]

这里的 for number in numbers 负责依次取得原列表中的元素,前面的 number * 2 决定新列表中保存什么内容。

阅读推导式时,可以先看后半部分的循环,再回到前面看每次要放入什么值:

python 复制代码
[number * 2 for number in numbers]

也就是"遍历 numbers,每次取出 number,把 number * 2 放入新列表"。

二、在推导式中筛选数据

如果只想保留符合条件的元素,可以在末尾加上 if

python 复制代码
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = [number for number in numbers if number % 2 == 0]

print(even_numbers)

输出结果为:

复制代码
[2, 4, 6]

这种写法的结构是:

python 复制代码
[表达式 for 变量 in 可迭代对象 if 条件]

执行顺序仍然是先遍历,再判断。只有条件为 True 的元素,才会计算前面的表达式并放入新列表。

例如,下面从多个文件名中找出 Python 文件:

python 复制代码
files = ["main.py", "README.md", "config.json", "app.py"]
python_files = [filename for filename in files if filename.endswith(".py")]

print(python_files)

输出结果为:

复制代码
['main.py', 'app.py']

推导式生成的是一个新列表,不会修改原来的 files

三、先处理,再筛选

表达式和筛选条件可以同时出现:

python 复制代码
numbers = [1, 2, 3, 4, 5, 6]
squares = [number ** 2 for number in numbers if number % 2 == 0]

print(squares)

这段代码先从 numbers 中筛选偶数,再计算它们的平方,结果为:

复制代码
[4, 16, 36]

不要按照从左到右的顺序机械理解这一行。实际过程可以写成下面这样:

python 复制代码
squares = []

for number in numbers:
    if number % 2 == 0:
        squares.append(number ** 2)

普通循环和推导式的结果相同。推导式只是把这个过程压缩了,并没有改变循环和判断的逻辑。

四、推导式中的 if...else

有时并不是丢掉不符合条件的元素,而是想根据条件生成不同的值。这时要把 if...else 写在 for 前面:

python 复制代码
scores = [86, 52, 91, 48]
results = ["及格" if score >= 60 else "不及格" for score in scores]

print(results)

输出结果为:

复制代码
['及格', '不及格', '及格', '不及格']

它的结构是:

python 复制代码
[条件成立时的值 if 条件 else 条件不成立时的值 for 变量 in 可迭代对象]

这里的 if...else 属于条件表达式,每次遍历都会产生一个值,所以原列表有多少项,新列表通常也有多少项。

下面两种写法看起来相似,不过用途不同:

python 复制代码
# 筛选:只保留偶数
[number for number in numbers if number % 2 == 0]

# 二选一:偶数保留原值,奇数改成 0
[number if number % 2 == 0 else 0 for number in numbers]

第一种写法可能减少元素数量,第二种写法会为每个元素生成一个结果。

五、多层列表推导式

推导式中可以写多个 for。例如,需要组合两组数据:

python 复制代码
colors = ["黑色", "白色"]
sizes = ["S", "M", "L"]

products = [
    f"{color}-{size}"
    for color in colors
    for size in sizes
]

print(products)

输出结果为:

复制代码
['黑色-S', '黑色-M', '黑色-L', '白色-S', '白色-M', '白色-L']

多个 for 的顺序与普通嵌套循环一致:

python 复制代码
products = []

for color in colors:
    for size in sizes:
        products.append(f"{color}-{size}")

外层循环写在前面,内层循环写在后面。两层还比较容易读,继续增加层数以后通常就不适合写成推导式了。

六、把二维列表展开

多层推导式还有一个常见用法,就是把二维列表展开成一维列表:

python 复制代码
rows = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

numbers = [number for row in rows for number in row]

print(numbers)

输出结果为:

复制代码
[1, 2, 3, 4, 5, 6, 7, 8, 9]

它对应的普通循环是:

python 复制代码
numbers = []

for row in rows:
    for number in row:
        numbers.append(number)

判断多个 for 的顺序时,最稳妥的方法就是先把它还原成普通循环。

七、字典推导式

字典推导式使用花括号,并且要同时写出键和值:

python 复制代码
numbers = [1, 2, 3, 4]
squares = {number: number ** 2 for number in numbers}

print(squares)

输出结果为:

复制代码
{1: 1, 2: 4, 3: 9, 4: 16}

基本结构是:

python 复制代码
{键表达式: 值表达式 for 变量 in 可迭代对象}

字典推导式同样可以添加筛选条件。例如,只保留成绩及格的学生:

python 复制代码
scores = {
    "张三": 86,
    "李四": 52,
    "王五": 91,
}

passed = {
    name: score
    for name, score in scores.items()
    if score >= 60
}

print(passed)

输出结果为:

复制代码
{'张三': 86, '王五': 91}

这里通过 items() 同时取得姓名和成绩,再把符合条件的键值对放入新字典。

字典推导式还可以交换键和值:

python 复制代码
extensions = {
    "Python": ".py",
    "JavaScript": ".js",
    "Markdown": ".md",
}

extension_names = {
    extension: name
    for name, extension in extensions.items()
}

不过,原字典的值如果有重复,交换以后会变成相同的键,前面的内容会被后面的内容覆盖。

八、集合推导式

集合推导式的写法与列表推导式很像,只是把方括号换成花括号:

python 复制代码
words = ["Python", "python", "Java", "java", "PYTHON"]
languages = {word.lower() for word in words}

print(languages)

集合会自动去重,因此结果中只有 pythonjava。集合本身没有固定顺序,实际打印顺序可能不同。

基本结构是:

python 复制代码
{表达式 for 变量 in 可迭代对象}

集合推导式也可以筛选:

python 复制代码
numbers = [1, 2, 2, 3, 4, 4, 5]
even_numbers = {number for number in numbers if number % 2 == 0}

print(even_numbers)

最终得到 {2, 4}

九、元组没有推导式

把推导式外面的括号换成圆括号,并不会创建元组:

python 复制代码
numbers = (number * 2 for number in range(5))

print(numbers)

这里得到的是生成器对象,不是元组。生成器会在后面的迭代器与生成器章节中单独讲。

目前如果需要元组,可以先使用列表推导式,再通过 tuple() 转换:

python 复制代码
numbers = tuple([number * 2 for number in range(5)])

print(numbers)

也可以把生成器交给 tuple()

python 复制代码
numbers = tuple(number * 2 for number in range(5))

这一行现在知道结果即可,生成器的执行方式后面再展开。

十、使用 range() 生成数字

range() 在前面的循环章节中已经用过,它会生成一段整数序列,常见写法有三种:

python 复制代码
range(5)
range(2, 6)
range(2, 10, 2)

分别表示:

复制代码
0, 1, 2, 3, 4
2, 3, 4, 5
2, 4, 6, 8

range(start, stop, step) 包含起始值 start,不包含结束值 stopstep 是步长,省略时为 1

需要倒序时,可以使用负数步长:

python 复制代码
for number in range(5, 0, -1):
    print(number)

结果会从 5 打印到 1

如果只是遍历列表中的元素,不需要为了使用索引而写 range(len(...))

python 复制代码
names = ["张三", "李四", "王五"]

# 只需要元素时,直接遍历
for name in names:
    print(name)

确实需要索引时,通常使用下面的 enumerate() 更清楚。

十一、使用 enumerate() 同时取得序号和元素

遍历列表时,如果同时需要元素的位置,可以使用 enumerate()

python 复制代码
names = ["张三", "李四", "王五"]

for index, name in enumerate(names):
    print(index, name)

输出结果为:

复制代码
0 张三
1 李四
2 王五

默认序号从 0 开始。如果显示给用户看的编号要从 1 开始,可以传入 start=1

python 复制代码
for index, name in enumerate(names, start=1):
    print(index, name)

输出结果为:

复制代码
1 张三
2 李四
3 王五

它比下面这种写法更直接:

python 复制代码
for index in range(len(names)):
    print(index, names[index])

range(len(names)) 并不是错误,只是当程序同时需要索引和元素时,enumerate() 更容易看懂,也不需要再写一次 names[index]

十二、使用 zip() 同时遍历多个对象

如果两组数据的位置一一对应,可以使用 zip() 把它们放在一起遍历:

python 复制代码
names = ["张三", "李四", "王五"]
scores = [86, 92, 78]

for name, score in zip(names, scores):
    print(name, score)

输出结果为:

复制代码
张三 86
李四 92
王五 78

也可以把结果转换成字典:

python 复制代码
score_dict = dict(zip(names, scores))
print(score_dict)

需要注意,多个对象长度不同时,zip() 会在最短的对象结束时停止:

python 复制代码
names = ["张三", "李四", "王五"]
scores = [86, 92]

print(list(zip(names, scores)))

结果中不会出现王五:

复制代码
[('张三', 86), ('李四', 92)]

如果数据长度本来就应该一致,实际使用前最好检查一下,否则多出来的内容可能会被忽略。

十三、遍历字典时使用 items()

上一章已经讲过,直接遍历字典时得到的是键:

python 复制代码
scores = {"张三": 86, "李四": 92, "王五": 78}

for name in scores:
    print(name)

如果键和值都要使用,就调用 items() 并进行解包:

python 复制代码
for name, score in scores.items():
    print(f"{name}:{score}")

只需要键时可以直接遍历字典,只需要值时使用 values()。没有必要一律写成 items(),根据循环中真正会用到的内容选择即可。

十四、使用 sorted() 排序后遍历

列表可以使用 sort() 原地排序,sorted() 则会返回一个新的列表,并且可以接收列表、元组、集合和字典等可迭代对象:

python 复制代码
numbers = [5, 2, 8, 1]
result = sorted(numbers)

print(result)
print(numbers)

输出结果为:

复制代码
[1, 2, 5, 8]
[5, 2, 8, 1]

按照从大到小排列,可以传入 reverse=True

python 复制代码
result = sorted(numbers, reverse=True)

字典默认按照键排序。需要按照值排序时,可以给 key 传入一个函数:

python 复制代码
scores = {"张三": 86, "李四": 92, "王五": 78}

for name, score in sorted(
    scores.items(),
    key=lambda item: item[1],
    reverse=True,
):
    print(name, score)

这里的 lambda 是匿名函数,后面会在函数章节详细讲。目前只需要知道 item 是一个 (姓名, 成绩) 元组,item[1] 表示让 sorted() 根据成绩排序。

如果不想提前接触 lambda,也可以先把内容转换成列表,再使用普通方式处理。基础阶段没有必要为了少写几行,把代码写得很难读。

十五、使用 reversed() 倒序遍历

需要从后向前遍历序列时,可以使用 reversed()

python 复制代码
names = ["张三", "李四", "王五"]

for name in reversed(names):
    print(name)

它不会修改原列表。如果需要得到一个新的倒序列表,可以转换为 list

python 复制代码
reversed_names = list(reversed(names))

列表切片 names[::-1] 也能得到倒序副本。只进行倒序遍历时,reversed(names) 更能直接说明代码的目的。

十六、循环中的解包

当列表中的每个元素本身又包含多个值时,可以在 for 后面直接解包:

python 复制代码
students = [
    ("张三", 86),
    ("李四", 92),
    ("王五", 78),
]

for name, score in students:
    print(name, score)

这与先取得整个元组,再通过索引读取的结果相同:

python 复制代码
for student in students:
    name = student[0]
    score = student[1]
    print(name, score)

解包要求变量数量与元素中的值数量对应,否则会出现 ValueError

python 复制代码
data = [("张三", 86, "一班")]

# 右侧有三个值,左侧只有两个变量
for name, score in data:
    print(name, score)

不需要其中某个值时,通常使用下划线 _ 占位:

python 复制代码
for name, _, class_name in data:
    print(name, class_name)

十七、推导式不要写得太复杂

推导式适合处理简单、明确的转换和筛选:

python 复制代码
prices = [99, 128, 56, 220]
discounted = [price * 0.9 for price in prices if price >= 100]

如果里面出现多层循环、多个条件或者复杂计算,普通循环通常更容易修改和调试。例如:

python 复制代码
results = []

for student in students:
    name = student["name"]
    score = student["score"]

    if score >= 90:
        level = "优秀"
    elif score >= 60:
        level = "及格"
    else:
        level = "不及格"

    results.append({"name": name, "level": level})

这段代码即使能够压缩成一行,也没有必要这样做。推导式的作用是减少简单重复,不是让代码行数越少越好。

十八、综合示例:整理学生成绩

下面把这一章的几种写法放在一起,完成一个简单的成绩整理:

python 复制代码
names = ["张三", "李四", "王五", "赵六"]
scores = [86, 52, 91, 78]

# 合并姓名和成绩
students = [
    {"name": name, "score": score}
    for name, score in zip(names, scores)
]

# 筛选及格学生
passed_students = [
    student
    for student in students
    if student["score"] >= 60
]

# 创建"姓名:成绩"字典
score_dict = {
    student["name"]: student["score"]
    for student in students
}

# 按成绩从高到低输出排名
sorted_students = sorted(
    students,
    key=lambda student: student["score"],
    reverse=True,
)

for rank, student in enumerate(sorted_students, start=1):
    print(rank, student["name"], student["score"])

print("及格人数:", len(passed_students))
print("成绩字典:", score_dict)

这里使用 zip() 把姓名和成绩按位置组合,列表推导式负责创建学生记录,字典推导式负责生成便于查询的成绩字典。最后使用 sorted() 排序,再用 enumerate() 生成从 1 开始的名次。

如果 namesscores 来自不同的数据源,还应该先检查长度是否一致:

python 复制代码
if len(names) != len(scores):
    print("姓名和成绩的数量不一致")

十九、常见错误

1. 把表达式写在 for 后面

python 复制代码
# 错误写法
[for number in numbers number * 2]

列表推导式要先写最终放入列表的表达式,再写 for

2. 筛选条件放错位置

python 复制代码
# 只筛选时,if 写在末尾
[number for number in numbers if number > 0]

# 二选一时,if...else 写在 for 前面
[number if number > 0 else 0 for number in numbers]

这两种 if 的作用不同,位置也不能互换。

3. 把圆括号理解成元组推导式

python 复制代码
result = (number * 2 for number in numbers)

它得到的是生成器,不是元组。需要元组时可以使用 tuple()

4. 使用 zip() 时忽略长度差异

zip() 会以最短的对象为准,多出的元素不会进入结果。如果长度必须一致,应该提前检查。

5. 为了缩短代码堆叠太多逻辑

当推导式已经很难一次读懂时,就应该改回普通循环。代码是否清楚,比是不是一行更重要。

二十、练习

练习一:生成平方列表

使用列表推导式生成 110 的平方,只保留平方结果大于 20 的数字。

练习二:整理文件名

给定一组文件名,找出所有以 .py 结尾的文件,并把文件名统一转换为小写。

练习三:生成成绩字典

分别创建姓名列表和成绩列表,使用 zip() 与字典推导式生成成绩字典。

练习四:统计单词长度

给定一个单词列表,使用字典推导式生成"单词:长度"的对应关系。

练习五:输出排名

使用 sorted() 对学生成绩排序,再使用 enumerate() 输出从 1 开始的名次。

相关推荐
暮雨哀尘1 小时前
Java 基础练习(一):创建类并调用对象
java·开发语言
zhangzeyuaaa1 小时前
Python requests:raise_for_status() 与 4xx/5xx 异常层次详解
开发语言·python
泡海椒2 小时前
动态代理核心原理:JQuick-Java接口规则自动生成机制解析
java·开发语言·python
余槐i2 小时前
Firecrawl 实战:将网站转换为大模型可用数据
人工智能·python·工具·firecrawl
shirsl2 小时前
算法 Day 2 滑动窗口 + 栈 / 单调栈
开发语言·python·算法
步行cgn2 小时前
Spring 启动报错:BeanFactory not initialized 的原因与解决
java·python·spring
菜鸟~noob2332 小时前
【电子战】第08篇:多普勒测向的工程实现与优化【含matlab代码】
开发语言·matlab
菜鸟~noob2332 小时前
【电子战】第06篇:干涉仪与相位差测向【含matlab代码】
开发语言·matlab
老王爱玩车2 小时前
深入理解指针2
c语言·开发语言·学习