3. 生成器表达式进阶:嵌套、条件过滤与 map/filter 对比
说实话,很多人学完生成器表达式基础后,就觉得它只是个省内存的列表推导式。但我在实际项目中慢慢发现,它的真正威力藏在「组合」和「筛选」里。这一章,我们就来聊聊嵌套生成器、条件过滤,以及它和 map/filter 到底该怎么选。
3.1 嵌套生成器表达式
先问个问题:你遇到过需要处理二维数据的情况吗?比如一个矩阵,或者一个 CSV 文件的行列数据。我最早的做法是写两层 for 循环,后来发现嵌套生成器表达式能写得既简洁又高效。
看个例子:
# 矩阵转置
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 用嵌套生成器表达式转置
transposed = tuple(tuple(row[i] for row in matrix) for i in range(3))
print(transposed)
# 输出: ((1, 4, 7), (2, 5, 8), (3, 6, 9))
这里外层生成器遍历列索引,内层生成器遍历每一行取出对应列的元素。注意,内层生成器 (row[i] for row in matrix) 是惰性的,它不会一次性生成所有值,而是等外层生成器迭代时才逐个计算。
再来看一个实际场景:扁平化嵌套列表。
nested = [[1, 2], [3, 4, 5], [6]]
# 扁平化
flat = (item for sublist in nested for item in sublist)
print(list(flat)) # [1, 2, 3, 4, 5, 6]
注意这里的写法顺序:外层循环 for sublist in nested 写在前面,内层循环 for item in sublist 写在后面。这和嵌套 for 循环的书写顺序是一致的。我刚开始也经常搞反,后来记住「先外层后内层」就再也没错过。
3.2 生成器表达式与条件过滤
条件过滤是生成器表达式最常用的场景之一。说白了,就是在迭代过程中只保留满足条件的元素。
基本语法很简单:
# 筛选出 1 到 100 中能被 3 整除且不能被 5 整除的数
result = (x for x in range(1, 101) if x % 3 == 0 and x % 5 != 0)
print(list(result))
# 输出: [3, 6, 9, 12, 18, 21, 24, 27, 33, 36, 39, 42, 48, ...]
你还可以组合多个条件:
data = [12, 45, 67, 89, 34, 23, 56, 78, 90]
# 筛选出大于 50 且是偶数的数
filtered = (x for x in data if x > 50 if x % 2 == 0)
print(list(filtered)) # [56, 78, 90]
多个 if 之间是「与」的关系,等价于 if x > 50 and x % 2 == 0。我个人习惯用 and 写在一起,更直观。
# 错误示例
items = [1, 2, 3, 4, 5]
gen = (x for x in items if x > threshold) # threshold 未定义
# 这行会报 NameError,因为 threshold 在创建时就被求值了
# 正确做法
threshold = 3
gen = (x for x in items if x > threshold)
items.append(6) # 修改外部列表
print(list(gen)) # 输出: [4, 5, 6] — 注意 6 也被包含了,因为生成器在迭代时才检查 items
3.3 map/filter 与生成器表达式的对比
很多初学者会问:有了 map 和 filter,为什么还要用生成器表达式?它们到底有什么区别?
我直接说结论:生成器表达式是 map 和 filter 的「超集」,而且可读性更好。
| 特性 | map/filter | 生成器表达式 |
|---|---|---|
| 语法简洁性 | 需要函数或 lambda | 直接写表达式 |
| 可读性 | 一般,尤其 lambda 复杂时 | 好,接近自然语言 |
| 组合多个操作 | 需要链式调用 | 一个表达式搞定 |
| 惰性求值 | 是 | 是 |
| 性能 | 略快(纯 C 实现) | 略慢(Python 层循环) |
| 适用场景 | 已有现成函数 | 需要自定义逻辑 |
来看几个对比的例子:
# 场景1:简单映射 — 平方
numbers = [1, 2, 3, 4, 5]
# map 方式
squared_map = map(lambda x: x**2, numbers)
# 生成器表达式方式
squared_gen = (x**2 for x in numbers)
print(list(squared_map)) # [1, 4, 9, 16, 25]
print(list(squared_gen)) # [1, 4, 9, 16, 25]
# 场景2:过滤 + 映射 — 筛选偶数并求平方
# map + filter 链式调用
result_chain = map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))
# 生成器表达式
result_gen = (x**2 for x in numbers if x % 2 == 0)
print(list(result_chain)) # [4, 16]
print(list(result_gen)) # [4, 16]
你想想看,哪个更容易理解?我个人觉得生成器表达式更直观,尤其是逻辑复杂的时候。
str.upper、int),用 map 没问题。但如果需要自定义 lambda,或者要同时做过滤和映射,生成器表达式是更好的选择。
不过,map 在性能上确实有优势。因为 map 是用 C 实现的,而生成器表达式是在 Python 层循环。我在处理超大规模数据时,会先用 map 做基准测试,如果性能瓶颈明显,再考虑优化。
3.4 知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
3.5 实战建议
最后,分享几个我在项目中积累的经验:
- 优先用生成器表达式:除非你明确需要列表的全部元素,否则默认用生成器。我见过太多人用列表推导式处理百万级数据,结果内存爆了。
- 嵌套时注意可读性:如果嵌套生成器超过两层,或者条件逻辑复杂,我会把它拆成生成器函数,用
yield逐条产出。 - 性能敏感时做基准测试:map 确实快,但差距通常只有微秒级。除非你的数据量在千万级以上,否则可读性比那点性能更重要。
- 小心副作用:生成器表达式里不要做打印、修改外部变量等操作。它应该是纯函数式的。