Python 生成器表达式:惰性计算与短路判断

2026-09-07 00:00    #Python   #语法   #算法竞赛  

生成器表达式(Generator Expression)适合表示“按需产生的一串数据”。它的写法和列表推导式很接近,但不会立即把全部结果存入列表。

在算法验证代码中,生成器经常和 summinmaxanyallnext 配合:候选只产生一次,找到答案时还可以提前停止。

从列表推导式开始

列表推导式使用方括号:

1squares = [x * x for x in range(5)]
2
3assert squares == [0, 1, 4, 9, 16]

执行到这一行时,Python 已经计算并保存了全部五个结果。它相当于:

1squares = []
2for x in range(5):
3    squares.append(x * x)
4
5assert squares == [0, 1, 4, 9, 16]

生成器表达式把方括号换成圆括号:

1squares = (x * x for x in range(5))
2
3assert next(squares) == 0
4assert next(squares) == 1
5assert list(squares) == [4, 9, 16]

创建 squares 时并没有计算五个平方。每次调用 next,生成器才产生下一个值;最后的 list 消费剩余三个值。

惰性求值

生成器只保存“如何产生下一个值”以及当前执行位置,这叫惰性求值(Lazy Evaluation)。

 1def produce():
 2    for x in range(3):
 3        yield x * x
 4
 5
 6values = produce()
 7
 8assert next(values) == 0
 9assert next(values) == 1
10assert next(values) == 4
11assert next(values, None) is None

yield 编写的是生成器函数,生成器表达式则是常见场景下的简写。本文重点使用表达式;理解二者都按需产生值即可。

生成器的主要特点是:

anyall 配合短路

any 判断是否至少有一个元素为真。它遇到第一个真值就返回:

 1visited = []
 2
 3
 4def is_even(x):
 5    visited.append(x)
 6    return x % 2 == 0
 7
 8
 9answer = any(is_even(x) for x in range(1, 10_000_000))
10
11assert answer
12assert visited == [1, 2]

这个具体输入在检查 12 后就停止,因此实际只执行两次判断。但算法的最坏复杂度仍是 O(n)O(n):如果直到最后一个元素才满足,或者没有元素满足,any 仍要遍历整个序列。

如果写成列表推导式,列表会先计算全部布尔值,之后 any 才能开始判断:

1values = [x % 2 == 0 for x in range(1, 6)]
2
3assert values == [False, True, False, True, False]
4assert any(values)

all 判断是否所有元素都为真,遇到第一个假值就停止:

1def is_strictly_increasing(a):
2    return all(x < y for x, y in zip(a, a[1:]))
3
4
5assert is_strictly_increasing([1, 3, 5, 9])
6assert not is_strictly_increasing([1, 3, 3, 9])
空序列

any([]) 返回 Falseall([]) 返回 True。这分别对应“空集合中不存在满足条件的元素”和“空集合中没有元素违反条件”。

与聚合函数配合

只需要最终聚合结果时,不必创建中间列表:

1a = [3, -2, 5, -1]
2
3positive_sum = sum(x for x in a if x > 0)
4largest_square = max(x * x for x in a)
5
6assert positive_sum == 8
7assert largest_square == 25

生成器作为函数的唯一参数时,可以省略外层圆括号:

1a = [1, 2, 3, 4]
2
3with_parentheses = sum((x * x for x in a))
4without_parentheses = sum(x * x for x in a)
5
6assert with_parentheses == without_parentheses == 30

如果候选可能为空,要处理 minmax 的默认值:

1a = [-3, -1, -7]
2
3answer = max((x for x in a if x > 0), default=None)
4
5assert answer is None

使用 next 找到具体方案

any 只能告诉我们是否存在,next 可以返回第一个满足条件的候选:

 1def first_pair_with_sum(a, target):
 2    return next(
 3        (
 4            (a[i], a[j])
 5            for i in range(len(a))
 6            for j in range(i + 1, len(a))
 7            if a[i] + a[j] == target
 8        ),
 9        None,
10    )
11
12
13assert first_pair_with_sum([2, 7, 11, 15], 9) == (2, 7)
14assert first_pair_with_sum([1, 2, 3], 10) is None

next(generator, None) 在生成器为空时返回 None。如果省略默认值,生成器为空会抛出 StopIteration

生成器只能消费一次

1values = (x * x for x in range(4))
2
3assert sum(values) == 14
4assert sum(values) == 0

第一次 sum 已经取完了全部元素,第二次不会重新开始。需要多次遍历时有两种选择:

 1def make_values():
 2    return (x * x for x in range(4))
 3
 4
 5assert sum(make_values()) == 14
 6assert sum(make_values()) == 14
 7
 8saved = list(make_values())
 9assert sum(saved) == 14
10assert max(saved) == 9

什么时候使用列表

生成器不是列表的替代品。以下情况应直接使用列表:

以下情况适合生成器:

选择标准

列表强调“保存结果”,生成器强调“产生结果”。根据后续操作选择,不要为了 Pythonic 而强行改成生成器。

在暴力验证中的位置

生成器擅长简洁地表达候选,但决定验证程序正确性的仍然是枚举范围和判定条件。复杂状态变化应先写成清楚的循环或 DFS,再考虑是否适合用生成器压缩。

更多枚举模型见用 Python 快速编写算法暴力验证程序,更多迭代器组合见itertools 实用组合

参考资料