处理大量数据时,先把所有结果装进列表再慢慢用,往往会白白吃掉大量内存。Python 的生成器(generator)提供了一种"用到才产出"的惰性思路:数据一个一个给,内存里永远只放当前这一个。本篇讲清楚生成器是什么、怎么写、什么时候用。
为什么需要生成器
先看一组对比。列表推导式会立刻把所有元素算出来放进内存:
nums = [x * x for x in range(5)]
print(nums) # 输出:[0, 1, 4, 9, 16]
gen = (x * x for x in range(5)) # 只是把 [] 换成 ()
print(gen) # 输出:<generator object <genexpr> at 0x...>
print(next(gen)) # 输出:0
print(next(gen)) # 输出:1
圆括号版本叫生成器表达式,它返回的不是列表,而是一个生成器对象。调用 next() 时它才算出下一个值——数据是"按需生产"的。如果序列有一千万个元素,列表要把一千万个结果全存下来,生成器却始终只保存"当前算到哪了"。
yield:把普通函数变成生成器
函数体里只要出现 yield 关键字,这个函数就不再是普通函数,而是"生成器工厂":调用它不会执行函数体,而是返回一个生成器对象。
def count_down(n):
while n > 0:
yield n # 每次执行到 yield 就暂停,交出 n
n -= 1
for v in count_down(3):
print(v)
# 输出:
# 3
# 2
# 1
它的执行流程值得单独说一遍:
- 调用
count_down(3)时不执行任何代码,只拿到生成器。 - 第一次迭代(
for或next()),代码从头执行到yield n,交出3后原地暂停。 - 下一次迭代从暂停处继续:执行
n -= 1,再循环回yield,交出2…… - 函数自然结束时抛出
StopIteration,for循环捕获它并正常收尾。
也就是说,yield 相当于给函数按下了"暂停键",下次从暂停处接着播。这正是它和 return 的本质区别:return 是结束,yield 是暂停。
惰性求值的经典场景:逐行读大文件
生成器最适合"数据流"式的处理,比如逐行读取一个很大的日志文件——不必把整个文件读进内存:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip() # 产出一行,暂停,等下一次要
# 内存中任何时刻只有当前这一行
for line in read_lines("access.log"):
if "ERROR" in line:
print(line)
再比如生成一个超大序列做统计,用生成器几乎不占额外内存:
total = sum(x * x for x in range(1_000_000))
print(total) # 输出:333332833333500000
注意 sum() 里直接放生成器表达式,连括号都能省一层,这也是 Python 圈推崇的写法。
两个必须知道的特性
生成器是一次性的。 值被取走就没有了,不能回头重新遍历:
gen = (x for x in [1, 2, 3])
print(list(gen)) # 输出:[1, 2, 3]
print(list(gen)) # 输出:[](已经耗尽)
可以用 list() 随时把生成器"倒"成列表。 数据量不大、需要反复使用时,直接转列表更方便;数据量大、只过一遍时,保持生成器形态最省内存。
小结
- 生成器是惰性产数据的对象:生成器表达式
(x*2 for x in ...)与含yield的函数都能创建它。 yield是暂停键:每次迭代执行到yield交出一个值并停住,下次从断点继续,函数结束抛StopIteration。- 逐行读大文件、流式处理超大序列是生成器的经典场景,内存占用与数据总量无关。
- 生成器只能遍历一次;需要反复使用就
list()转成列表。
评论 0
还没有评论,来抢沙发吧~ 🛋