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) 是惰性的,它不会一次性生成所有值,而是等外层生成器迭代时才逐个计算。

我的习惯:嵌套生成器表达式最多嵌套两层。超过两层,可读性会急剧下降。这时候我宁愿拆成具名生成器函数,或者用 for 循环。

再来看一个实际场景:扁平化嵌套列表。

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 知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系:

生成器表达式进阶知识体系 生成器表达式进阶 嵌套生成器 条件过滤 map/filter 对比 矩阵转置 扁平化列表 单条件筛选 多条件组合 语法对比 性能对比 核心要点 • 嵌套生成器最多两层,超过建议拆分为具名函数 • 条件过滤注意外部变量的修改时机 • 有现成函数用 map,自定义逻辑用生成器表达式

3.5 实战建议

最后,分享几个我在项目中积累的经验:

  • 优先用生成器表达式:除非你明确需要列表的全部元素,否则默认用生成器。我见过太多人用列表推导式处理百万级数据,结果内存爆了。
  • 嵌套时注意可读性:如果嵌套生成器超过两层,或者条件逻辑复杂,我会把它拆成生成器函数,用 yield 逐条产出。
  • 性能敏感时做基准测试:map 确实快,但差距通常只有微秒级。除非你的数据量在千万级以上,否则可读性比那点性能更重要。
  • 小心副作用:生成器表达式里不要做打印、修改外部变量等操作。它应该是纯函数式的。