Python 生成器:边算边给的"懒人"数据工厂
引言:点菜的两种方式
你去餐厅吃饭,有两种点菜方式:
- 方式一:让厨师把 100 道菜全部做好,摆满一桌子,你再挑着慢慢吃。桌子再大也摆不下,而且你可能吃两口就饱了,剩下的全浪费;
- 方式二:你点一道,厨师做一道,你边吃边点。厨房不占地方,你也能随时停。
Python 的**生成器(Generator)**就是方式二——不一次性生成所有数据,而是你要一个,它算一个。这在处理海量数据时,是救命级的技巧。
一、为什么需要生成器?内存的痛点
1.1 列表的局限
上一篇我们学了列表生成式,一行代码就能造出一个列表:
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]但如果要生成 100 万个平方数呢?
>>> big_list = [x * x for x in range(1000000)] # 内存瞬间吃掉几十 MB更惨的是,如果你只需要前 10 个,后面 99 万多个元素占用的内存就全白扔了。
生活化理解:就像你只想喝一杯水,却买了整个水库的水倒进家里——杯子满了,水库空了,你还没喝完。
1.2 生成器的解决方案
如果列表元素能按某种算法推算出来,那我们能不能边循环边计算呢?这样就不必创建完整列表,内存占用始终是一个元素的量级。
在 Python 中,这种"一边循环一边计算"的机制,就叫生成器(Generator)。
生活化理解:生成器就像一个现做现卖的煎饼摊——你要一个,他摊一个;你不要,他不摊。锅里永远只有一个煎饼,而不是提前摊好 100 个堆在那里。
二、创建生成器:两种方法
2.1 方法一:把 [] 改成 ()
列表生成式用方括号 [],生成器表达式用圆括号 ():
>>> L = [x * x for x in range(10)] # 列表
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10)) # 生成器
>>> g
<generator object <genexpr> at 0x1022ef630>关键区别:
L是一个真实的列表,所有元素已经在内存里;g是一个生成器对象,里面一个元素都没有,只保存了"怎么算"的算法。
2.2 怎么取出元素?next() 和 for 循环
生成器不能直接用索引 g[0] 取值,因为它根本没有"第 0 个"——得现算。
方式一:手动 next()
>>> g = (x * x for x in range(10))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
... # 一直调用,直到
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
...
StopIteration每次 next(g),生成器就算出下一个值;算完最后一个,再调用就抛 StopIteration 错误。
方式二:for 循环(推荐)
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
...
81生活化理解:next() 像你手动按一下出餐铃,服务员上一道菜;for 循环像你开了自动模式,服务员一道接一道上,直到你说"停"。
注意:生成器是一次性的。上面的
for循环跑完后,g就空了,再for一次不会有任何输出。
三、方法二:用函数 + yield 创建生成器
3.1 什么时候需要函数式生成器
生成器表达式适合简单场景,但如果算法比较复杂(比如斐波那契数列),for 循环写不出来,就得用函数。
3.2 斐波那契数列:从普通函数到生成器
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, ...(除前两个数外,每个数都是前两个数之和)
普通函数版本:用 print 打印
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'神奇的一行:a, b = b, a + b
这行代码同时给 a 和 b 赋新值,不需要临时变量。等价于:
t = (b, a + b) # 先打包成元组
a = t[0]
b = t[1]生成器版本:把 print(b) 改成 yield b
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 唯一改动!
a, b = b, a + b
n = n + 1
return 'done'调用方式完全不同:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>普通函数调用直接返回结果;生成器函数调用返回一个生成器对象。
3.3 yield 的魔法:暂停与继续
生成器函数的执行流程和普通函数完全不同:
- 普通函数:从第一行执行到
return或最后一行,然后结束; - 生成器函数:每次
next()执行到yield就暂停,把值扔出去;下次next()从上次暂停的地方继续执行。
生活化理解:就像看电视剧——普通函数是连续看完整部电影;生成器函数是"看一集暂停,去干别的,下次接着看下一集"。
3.4 逐帧观察:odd() 生成器
def odd():
print('step 1')
yield 1
print('step 2')
yield 3
print('step 3')
yield 5>>> o = odd()
>>> next(o)
step 1
1 # 执行到 yield 1,暂停
>>> next(o)
step 2
3 # 从上次暂停处继续,执行到 yield 3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
...
StopIteration # 没有更多 yield 了,结束关键洞察:print('step 1') 只在第一次 next() 时执行,后面不再重复——因为函数从 yield 处继续,而不是从头开始。
四、for 循环与 return 值
4.1 for 循环拿不到 return 值
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8fib(6) 的 return 'done' 哪去了?for 循环拿不到生成器的 return 值。
4.2 捕获 return 值:try...except
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: donereturn 的值藏在 StopIteration 异常的 value 属性里。
五、实战应用场景
5.1 读取大文件
def read_large_file(file_path):
"""逐行读取大文件,不占用全部内存"""
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()
# 使用:文件有 100 万行,内存里始终只有 1 行
for line in read_large_file('huge_log.txt'):
process(line)5.2 生成无限序列
def infinite_counter():
"""生成无限的自然数序列"""
n = 0
while True:
yield n
n += 1
counter = infinite_counter()
print(next(counter)) # 0
print(next(counter)) # 1
print(next(counter)) # 2
# ... 永远不会内存爆炸,因为每次只算一个5.3 数据管道:逐层过滤
def numbers():
for i in range(1000000):
yield i
def even(nums):
for n in nums:
if n % 2 == 0:
yield n
def square(nums):
for n in nums:
yield n * n
# 像流水线一样串联
result = square(even(numbers()))
for x in result:
if x > 100:
break
print(x)每个环节都是生成器,数据像水流一样逐层通过,内存占用始终极小。
六、经典练习:杨辉三角生成器
题目:用生成器输出杨辉三角的每一行。
def triangles():
row = [1]
while True:
yield row
# 根据当前行计算下一行
row = [1] + [row[i] + row[i+1] for i in range(len(row)-1)] + [1]验证:
n = 0
results = []
for t in triangles():
results.append(t)
n = n + 1
if n == 10:
break
for t in results:
print(t)输出:
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
...思路解析:
- 第 0 行固定是
[1]; - 每一行的首尾永远是
1; - 中间的每个数 = 上一行相邻两数之和;
- 用
yield把当前行抛出去,同时准备下一行。
七、常见误区与避坑指南
误区 1:以为生成器可以反复迭代
g = (x * x for x in range(3))
print(list(g)) # [0, 1, 4]
print(list(g)) # [] 空的!生成器已耗尽生成器是一次性的,迭代完就空了。如果要重复使用,得重新创建:
g = (x * x for x in range(3)) # 重新创建
print(list(g)) # [0, 1, 4]误区 2:直接 next(odd()) 导致每次都从头开始
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1 # 又是 1?!原因:odd() 每次都创建新的生成器对象,next() 当然返回第一个值。
正确做法:先保存生成器对象,再反复 next():
>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3误区 3:以为 yield 和 return 一样
def f():
yield 1
return 2 # 这个 2 不会被 for 循环拿到!
yield 3 # 永远不会执行yield:暂停,把值抛出去,下次继续;return:彻底结束函数,for循环直接终止,return 值只能通过捕获StopIteration拿到。
误区 4:在生成器表达式里用 return
# 错误:生成器表达式里没有 return
g = (x if x > 0 else return for x in [1, -2, 3]) # SyntaxError!生成器表达式是简洁语法,不能写复杂逻辑。复杂场景请用 yield 函数。
误区 5:混淆生成器函数和普通函数
def f1():
return [1, 2, 3]
def f2():
yield 1
yield 2
yield 3
print(f1()) # [1, 2, 3] —— 普通函数,直接返回结果
print(f2()) # <generator ...> —— 生成器函数,返回生成器对象判断方法:函数体里有没有 yield。有 yield 就是生成器函数,调用返回生成器对象;没有就是普通函数,调用直接返回结果。
八、动手练习
- 基础题:写一个生成器
countdown(n),从 n 倒数到 1,每次 yield 一个数。 - 进阶题:写一个生成器
fib_gen(),用yield生成斐波那契数列的前 10 个数,然后用for循环打印。 - 挑战题:写一个生成器
read_words(file_path),逐行读取文件,再逐行把每行拆成单词 yield 出去,实现"逐词读取"大文件。 - 思考题:
[x * 2 for x in range(5)]和(x * 2 for x in range(5))有什么区别?分别用在什么场景?
小结
- 生成器(Generator):边循环边计算,不一次性生成所有元素,极大节省内存。
- 两种创建方式:生成器表达式
(x * x for x in range(10));生成器函数(含yield的函数)。 - 执行机制:每次
next()执行到yield暂停,下次从暂停处继续;没有更多yield时抛StopIteration。 - 最佳实践:永远用
for循环迭代生成器,不要手动调next()。 - 一次性:生成器迭代完就空了,不能重复使用。
- 核心心法:生成器是"懒人"哲学——只在你真正需要的时候才干活,而且只干一点点。处理大数据、无限序列、数据管道时,它是唯一可行的方案。