生成器是 Python 中一种特殊的迭代器,它让你用函数的写法来实现迭代逻辑,但执行时不会一次性把所有结果算出来,而是用到的时候才计算下一个值。这种“按需计算”的策略就是惰性求值。
生成器函数:用 yield 代替 return
- 定义生成器的语法和普通函数几乎一样,只是不再用
return返回所有结果,而是用yield逐个“产出”结果。 - 当函数体中出现
yield关键字时,该函数就不再是普通函数,而是生成器函数。调用生成器函数并不会执行函数体,而是返回一个生成器对象。
一个最简单的例子:
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
gen = count_up_to(3) # 这里不会执行函数体,只是创建了一个生成器对象
print(next(gen)) # 输出 1
print(next(gen)) # 输出 2
print(next(gen)) # 输出 3
# 再调用 next() 会抛出 StopIteration 异常
yield 关键字的执行流程
每次调用 next()(或者在 for 循环中每次迭代)时:
- 生成器函数从头开始执行,或从上次暂停的地方继续执行。
- 遇到
yield时,把yield后面的值返回给外部调用者,函数状态暂停并保留所有局部变量。 - 当再次调用
next()时,函数从暂停处继续往下执行,直到再次遇到yield,以此类推。 - 如果函数执行完也没有再遇到
yield,则自动抛出StopIteration,循环就会正常结束。
这就像一个可以多次“中断再恢复”的函数,非常形象。
惰性求值:省内存、能处理无限序列
“惰性”意味着延迟计算——数据只在真正需要的那一刻才被生成,而不是提前全部算好放在列表里。
- 处理大文件:比如读取一个几 GB 的日志文件,如果用
f.readlines()会把全部内容加载到内存,很可能爆内存。而把文件对象当作迭代器(或写一个逐行 yield 的生成器),每次只读入一行处理一行,内存占用只有一行的大小。 - 无限序列:生成器可以产出无穷无尽的值(比如所有自然数、斐波那契数列),只要外部循环来控制何时停止。如果用具象列表,预先生成无限个值是不可能的。
def infinite_numbers():
n = 0
while True:
yield n
n += 1
- 管道式组合:多个生成器可以像管道一样串联,前一个生成器的输出成为后一个生成器的输入。每一层都是按需计算的,整个链条的内存消耗极小。
生成器表达式:更简洁的语法
和列表推导式类似,换成圆括号就成了生成器表达式。它不会立即计算所有元素,而是返回一个生成器对象。
squares = (x * x for x in range(10)) # 生成器对象
print(next(squares)) # 0
print(next(squares)) # 1
# 可以直接用在 for 循环中
for sq in (x * x for x in range(5)):
print(sq)
与列表推导式的区别:
[x*x for x in range(1000000)]会立刻在内存中创建 100 万个元素的列表,可能占用几百 MB 内存。(x*x for x in range(1000000))只是一个生成器,内存占用几乎可以忽略不计,计算在迭代时实时进行。
实际应用场景
- 文件逐行读取与过滤:例如,读取大日志文件,找出包含 “ERROR” 的行,而不必加载整个文件。
- 数据流水线:读取数据 → 清洗数据 → 转换格式 → 输出结果,每一步都用生成器,整条流水线内存高效。
- 替代临时列表:在很多只需要遍历一次的场景下,用生成器表达式代替列表推导式可以显著降低内存消耗。例如
sum(x*2 for x in range(1000000)),直接用生成器传给sum(),无需先创建一个百万元素的列表。
本节关键总结
- 生成器函数:用
yield逐个产出值,调用时返回生成器对象,支持next()或直接用于for循环。 - yield 执行模型:暂停并保存现场,等待下一次
next()时恢复执行。 - 惰性求值:按需生成数据,避免一次性加载所有数据到内存,非常适合处理大数据量和无限序列。
- 生成器表达式:语法更紧凑的生成器创建方式,场景和列表推导式类似,但内存友好。