生成器表达式是 Python 中按需产生数据的一种轻量语法。你可以把遍历、筛选和转换都写在一个表达式里,却不用一次性创建并保存所有结果——数据是在迭代过程中逐个生成的。
它的结构和列表推导式很相似,但使用的是圆括号 (),返回的是生成器对象。当你只需要遍历一次、数据规模较大,或者希望在处理过程中提前结束时,生成器表达式再合适不过了。
一、什么是生成器表达式
1、基本语法
生成器表达式使用圆括号,基本形式是:
(输出表达式 for 变量 in 可迭代对象)
比如,我们要产生 0 到 4 的平方:
squares = (
x ** 2
for x in range(5)
)
print(squares)
你会看到类似这样的输出:
<generator object <genexpr> at 0x...>
它并没有直接输出平方数值,而是给了一个生成器对象。
这段代码可以拆成三部分:
x ** 2 # 计算当前要产生的元素
for x # 将当前元素依次绑定给 x
in range(5) # 指定要遍历的数据
如果想手动控制迭代,可以用 next() 一个个获取元素:
print(next(squares))
print(next(squares))
print(next(squares))
输出:
0
1
4
生成器表达式可以看作简化版的生成器函数。上面的代码与下面这段函数的效果差不多:
def generate_squares():
for x in range(5):
yield x ** 2
squares = generate_squares()
生成器函数通过 yield 逐个产出结果,而生成器表达式就是把这种简单逻辑写在一个表达式里。
2、生成器表达式的结果特点
(1)结果是生成器对象
生成器表达式不会变成列表、元组或集合,它返回的就是一个生成器对象:
values = (x ** 2 for x in range(5))
print(type(values))
输出:
<class 'generator'>
所以,像下面这样的写法,别误以为是元组推导式:
values = (x ** 2 for x in range(5))
Python 压根就没有专门的元组推导式。想要元组的话,可以用 tuple() 把生成器表达式包起来:
values = tuple(x ** 2 for x in range(5))
print(values)
输出:
(0, 1, 4, 9, 16)
(2)元素按需产生
生成器表达式采用惰性计算:创建生成器时,通常不会立刻计算所有元素;只有当你请求下一个元素时,对应的计算才会真正执行。
看个例子就明白了:
def double(value):
print(f"处理 {value}")
return value * 2
values = (
double(x)
for x in range(3)
)
print("生成器已创建")
print(next(values))
print(next(values))
输出:
生成器已创建
处理 0
0
处理 1
2
生成器创建的时候,double() 根本没被调用。只有每次 next() 的时候,才会运行一次计算。
(3)生成器通常只能遍历一次
生成器会记住当前迭代位置,取出来的元素就溜走了,不会自动归位:
values = (x ** 2 for x in range(5))
print(list(values))
print(list(values))
输出:
[0, 1, 4, 9, 16]
[]
第一次 list() 就取光了所有元素,第二次再遍历,生成器已经空了。
如果生成器耗尽后你还继续调用 next(),就会触发 StopIteration:
values = (x for x in range(1))
print(next(values))
print(next(values)) # 这一次会抛 StopIteration
第一次 next() 得到 0,再调用就引发异常。不过用 for 循环遍历时,for 会把 StopIteration 当成结束信号,自动结束循环,程序并不会因为这个异常中断。
(4)不支持索引和长度查询
生成器不一次性存着全部元素,所以没办法用索引访问:
values = (x ** 2 for x in range(5))
print(values[0]) # TypeError
len() 也不行:
print(len(values)) # TypeError
如果你需要反复遍历、按索引取值或者提前知道元素数量,列表才是更合适的选择。
3、作为函数参数
生成器表达式经常直接作为函数参数,尤其在聚合计算时非常方便。
比如,求 0 到 4 的平方和:
total = sum(
x ** 2
for x in range(5)
)
print(total) # 30
当生成器表达式是函数调用里唯一的参数时,它自己的圆括号可以省略:
total = sum(x ** 2 for x in range(5))
这里的括号属于 sum() 函数调用,并不会把生成器表达式变成元组。
下面这两种写法完全等价:
total = sum(x ** 2 for x in range(5))
total = sum((x ** 2 for x in range(5)))
第一种更简洁,也最常用。
如果函数还接收别的参数,生成器表达式最好加上自己的括号,比如:
values = sorted(
(x ** 2 for x in range(5)),
reverse=True
)
print(values) # [16, 9, 4, 1, 0]
sum()、max()、min()、any()、all() 这些函数都很喜欢和生成器表达式搭档,用来做聚合计算或条件判断。你也可以用 list()、tuple() 把生成器里的数据一次性展开成新的容器对象。
二、带条件的生成器表达式
生成器表达式里的条件有两种常见用途:条件筛选和条件表达式。if 放的位置不一样,作用也不同。
1、条件筛选
在 for 后面加上 if,可以只处理符合条件的元素。基本语法长这样:
(输出表达式 for 变量 in 可迭代对象 if 条件)
比如,只生成偶数的平方:
even_squares = (
x ** 2
for x in range(10)
if x % 2 == 0
)
for value in even_squares:
print(value)
输出:
0
4
16
36
64
只有被 2 整除的数字才会参与结果生成。
2、条件表达式
条件表达式可以放在输出表达式的位置,根据条件选择不同的结果。语法如下:
(结果1 if 条件 else 结果2 for 变量 in 可迭代对象)
比如,按需生成奇偶标签:
labels = (
"even" if x % 2 == 0 else "odd"
for x in range(5)
)
print(list(labels))
输出:
['even', 'odd', 'even', 'odd', 'even']
你会发现,生成器表达式不会像集合推导式那样去重,而是老老实实按遍历顺序一个一个产出结果。
三、包含多个 for 子句的生成器表达式
生成器表达式里可以连续写多个 for 子句,表示多层循环。基本语法:
(输出表达式 for 变量1 in 可迭代对象1 for 变量2 in 可迭代对象2)
多个 for 从左到右对应嵌套循环由外到内的顺序。
举个例子,按需生成二维坐标点:
points = (
(x, y)
for x in range(2)
for y in range(3)
)
for point in points:
print(point)
输出:
(0, 0)
(0, 1)
(0, 2)
(1, 0)
(1, 1)
(1, 2)
这里 for x 是外层循环,for y 是内层循环,(x, y) 是当前产出的元素。
多层 for 特别适合展平嵌套数据。比如把二维列表压平:
matrix = [
[1, 2, 3],
[4, 5, 6]
]
values = (
value
for row in matrix
for value in row
)
print(list(values)) # [1, 2, 3, 4, 5, 6]
只有在你调用 list() 时,它才真正把所有数据推出来;如果用 for 循环,就会逐个处理,不会一下子全挤进内存。
四、生成器表达式的典型应用场景
生成器表达式天生适合那些“只跑一趟”的数据处理任务。它常常与聚合函数、短路判断函数、文件对象或其他生成器一起协作。
1、直接进行聚合计算
sum()、max() 和 min() 这类函数可以直接吃进生成器,不用先造一座列表的“桥梁”。
例如,计算 0 到 999999 的平方和:
total = sum(
x ** 2
for x in range(1_000_000)
)
print(total)
因为平方值是一个一个产生的,所以根本不用先存一百万个结果到列表里。
2、使用 any() 和 all() 提前结束判断
any() 在碰到第一个真值时就会收工,all() 在碰到第一个假值时也会停下。搭配生成器表达式,后面没必要的元素根本不会被检查。
比如,检查一组 AI 预测置信度中是否有低于 0.6 的:
confidences = [0.96, 0.91, 0.54, 0.88, 0.93]
has_low_confidence = any(
confidence < 0.6
for confidence in confidences
)
print(has_low_confidence) # True
any() 发现 0.54 < 0.6 之后,就能判定结果为 True,后面的数值不需要再管了。
类似地,想确认所有预测是否都达到最低要求,可以这样写:
all_confident = all(
confidence >= 0.5
for confidence in confidences
)
print(all_confident) # True
3、逐行处理文件数据
文件对象本身就是可迭代的,一行行吐给你。和生成器表达式配合,不用把整个文件读到内存就能边读边转、边筛。
假设 scores.txt 里每行记录一个分数:
with open("scores.txt", encoding="utf-8") as file:
scores = (
float(line.strip())
for line in file
if line.strip()
)
highest_score = max(scores)
print(highest_score)
这里,max() 请求数据时,生成器才去逐行读取、去空白、转浮点数。
⚠️ 注意:因为生成器依赖打开的文件,所以它的消费过程必须放在 with 代码块内完成。
4、构建惰性处理管道
多个生成器表达式可以串成一个流水线,把复杂处理拆成多个清晰的步骤。
numbers = range(20)
even_numbers = (
number
for number in numbers
if number % 2 == 0
)
squares = (
number ** 2
for number in even_numbers
)
large_squares = (
value
for value in squares
if value > 50
)
print(list(large_squares))
输出:
[64, 100, 144, 196, 256, 324]
这里三个生成器分别负责筛选偶数、计算平方、挑选大于 50 的结果。每个阶段都按需从上个阶段取数据,整个流程不会提前生成一堆中间列表。
五、常见问题及使用建议
1、不要重复使用已经耗尽的生成器
生成器一般只能完整遍历一次:
values = (x ** 2 for x in range(5))
print(sum(values)) # 30
print(sum(values)) # 0,生成器已空
如果确实需要重复遍历,要么重新创建一个生成器:
values1 = (x ** 2 for x in range(5))
values2 = (x ** 2 for x in range(5))
要么在数据量合理的情况下,干脆用列表。
2、需要索引或重复遍历时使用列表推导式
生成器表达式很适合逐个处理,但不擅长这些事:
- 通过索引访问元素
- 多次遍历同一批结果
- 使用切片
- 直接查询元素数量
- 长期保存全部结果
这些场景,列表推导式更对路:
values = [
x ** 2
for x in range(5)
]
该选生成器还是列表,得看后续怎么用,不能只看写法是否简短。
3、转换为容器后就不再节省内存
像下面这样,虽然用了生成器表达式,可 list() 最后还是会保存全部结果:
values = list(x ** 2 for x in range(1_000_000))
这并不会减少最终列表占用的内存。如果目的就是为了生成列表,直接写列表推导式反而更明白:
values = [
x ** 2
for x in range(1_000_000)
]
生成器表达式的空间优势,只在元素被逐个消费、不需要完整保存时才体现得出来。
4、注意错误可能在迭代时才出现
因为生成器是惰性的,创建的时候未必立刻执行输出表达式里的操作。
values = (
10 / x
for x in [2, 1, 0, 5]
)
这段代码一般不会立马报错。只有当你真去迭代到 x = 0 时,才会抛出 ZeroDivisionError:
for value in values:
print(value)
所以,生成器里的坑,往往是在消费数据时才暴露的。
5、避免副作用操作
生成器表达式的本职是按需产生数据,别让它去干打印、写文件、修改外部状态这类事儿。如果主要目的是执行副作用,老老实实用普通 for 循环。
📘 小结
生成器表达式可以一边遍历一边完成转换和筛选,按需一个一个产出元素。它特别适合聚合计算、短路判断、文件流处理和惰性数据管道。使用时牢记:生成器通常只能遍历一次,不支持索引和长度查询;只有当结果被逐个消费时,它的空间优势才能发挥出来。