第四章 · 数据结构

推导式与生成器

本节目标

学完这一节,你会知道:

  1. 推导式是什么,和普通循环有什么关系
  2. 如何写列表推导式、字典推导式和集合推导式
  3. 什么时候应该用普通循环而不是推导式
  4. 生成器表达式和列表推导式有什么区别
  5. 如何用推导式做简单的数据转换和过滤

推导式的目标不是“少写几行”,而是在简单场景下让数据处理更清楚。

先跑一个例子

新建文件 comprehension_demo.py,写入:

scores = [55, 80, 92, 67, 40, 88]

passed_scores = [score for score in scores if score >= 60]
score_levels = ["及格" if score >= 60 else "不及格" for score in scores]

print(passed_scores)
print(score_levels)

运行:

python3 comprehension_demo.py

你会看到:

[80, 92, 67, 88]
['不及格', '及格', '及格', '及格', '不及格', '及格']

推导式把“遍历、判断、生成新列表”写到了一行。

从普通循环到列表推导式

普通写法:

numbers = [1, 2, 3, 4, 5]
squares = []

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

print(squares)

列表推导式:

numbers = [1, 2, 3, 4, 5]
squares = [number ** 2 for number in numbers]

print(squares)

结构是:

[新值 for 变量 in 数据]

带条件的列表推导式

筛选偶数:

numbers = range(1, 11)
evens = [number for number in numbers if number % 2 == 0]

print(evens)

输出:

[2, 4, 6, 8, 10]

结构是:

[新值 for 变量 in 数据 if 条件]

if/else 写在前面

如果要为每个元素生成不同结果,if/else 写在前面。

numbers = [1, 2, 3, 4, 5]
labels = ["偶数" if n % 2 == 0 else "奇数" for n in numbers]

print(labels)

输出:

['奇数', '偶数', '奇数', '偶数', '奇数']

记住:

  • 只筛选:[x for x in data if 条件]
  • 二选一生成值:[A if 条件 else B for x in data]

字典推导式

把名字列表转成“名字 -> 长度”的字典:

names = ["Alice", "Bob", "Charlie"]
name_lengths = {name: len(name) for name in names}

print(name_lengths)

输出:

{'Alice': 5, 'Bob': 3, 'Charlie': 7}

筛选字典:

scores = {"小明": 85, "小红": 92, "小刚": 58}
passed = {name: score for name, score in scores.items() if score >= 60}

print(passed)

集合推导式

集合推导式会自动去重。

words = ["hello", "world", "python", "hello"]
lengths = {len(word) for word in words}

print(lengths)

输出:

{5, 6}

找出文本中出现过的字母:

text = "hello world"
letters = {char for char in text if char.isalpha()}

print(letters)

生成器表达式

列表推导式会立刻创建完整列表。

生成器表达式不会一次性生成所有数据,而是用到一个生成一个。

numbers = (i ** 2 for i in range(1, 6))

print(numbers)
print(next(numbers))
print(next(numbers))

输出类似:

<generator object ...>
1
4

如果想一次看完:

print(list(numbers))

注意:生成器被遍历一次后,就会被消耗。

生成器适合什么场景?

适合数据量很大、只需要遍历一次的场景。

total = sum(i ** 2 for i in range(1, 1000001))
print(total)

这里传给 sum() 的是生成器表达式,不需要先创建一个巨大列表。

常见搭配:

numbers = [1, 3, 5, 8, 9]

has_even = any(n % 2 == 0 for n in numbers)
all_positive = all(n > 0 for n in numbers)

print(has_even)
print(all_positive)

了解即可:yield

如果生成逻辑比较复杂,可以写生成器函数。

def countdown(n):
    while n > 0:
        yield n
        n -= 1


for number in countdown(5):
    print(number)

yield 会一次返回一个值,并记住函数执行到哪里。以后处理大文件时很有用。

逐行拆解

再看开头的例子:

passed_scores = [score for score in scores if score >= 60]

遍历 scores,只保留大于等于 60 的分数。

score_levels = ["及格" if score >= 60 else "不及格" for score in scores]

每个分数都会生成一个结果:及格或不及格。

自己改一改

comprehension_demo.py 改成:

names = ["alice", "bob", "charlie"]

upper_names = [name.upper() for name in names]
name_lengths = {name: len(name) for name in names}

print(upper_names)
print(name_lengths)

然后继续改:

  1. 只保留长度大于 3 的名字
  2. 生成一个集合,保存所有名字长度
  3. 用生成器表达式计算 1 到 100 的平方和

常见错误

1. 推导式写得太复杂

如果一行代码很难读,就改回普通循环。清楚比短更重要。

2. if 位置写错

筛选:

[n for n in numbers if n > 0]

二选一:

["正数" if n > 0 else "非正数" for n in numbers]

这两种写法的 if 位置不同。

3. 生成器只能遍历一次

gen = (i for i in range(3))

print(list(gen))
print(list(gen))

第二次会得到空列表,因为生成器已经被消耗了。

4. 误以为集合推导式有顺序

集合没有固定顺序。如果需要保持顺序,用列表。

小练习

练习 1:平方列表

生成 1 到 10 的平方列表。

练习 2:筛选长单词

给定单词列表,只保留长度大于 4 的单词。

练习 3:成绩字典

给定成绩字典,只保留及格学生。

参考答案

练习 1:

squares = [i ** 2 for i in range(1, 11)]
print(squares)

练习 2:

words = ["hi", "python", "code", "learning"]
long_words = [word for word in words if len(word) > 4]

print(long_words)

练习 3:

scores = {"小明": 85, "小红": 92, "小刚": 58}
passed = {name: score for name, score in scores.items() if score >= 60}

print(passed)

小结

这一节你学会了:

  1. 列表推导式可以简化简单的循环生成列表
  2. 字典推导式可以生成键值对
  3. 集合推导式可以生成不重复的数据
  4. 生成器表达式适合大数据量、只遍历一次的场景
  5. 推导式不要写得太复杂,复杂逻辑优先用普通循环

下一章我们会学习文件与 IO。你会把程序的数据保存到文件里,也能从文件读取内容继续处理。

推导式很帅,但别帅过头

推导式能把简单的数据转换写得很利索,但如果一行看完需要深呼吸,就该换回普通循环了。马哥建议你先练筛选偶数、转换大小写、过滤成绩,写得清楚比写得短更重要。

讨论 (0)

还没有评论,来抢沙发吧!