🐍 彻底搞懂 Python 生成器:从入门到”yield”深处

摘要:处理大数据内存爆炸?想要实现无限序列?Python 生成器(Generator)是你的必备武器。本文不仅讲解语法,更深入底层机制,带你从“会用”到“精通”。


在日常开发中,你是否遇到过这样的场景:

  • 试图读取一个 10GB 的日志文件,结果程序直接 MemoryError 崩溃。
  • 需要生成一个 斐波那契数列,却不知道何时停止,导致列表无限膨胀。
  • 写了一堆数据处理函数,代码耦合严重,难以维护。

如果中枪了,那么今天的主角 Python 生成器(Generator) 就是为你量身定做的解决方案。

很多人知道 yield 关键字,但真的理解它的状态保持机制双向通信能力吗?今天,我们就来一次深度剖析。


01 为什么需要生成器?

在理解生成器之前,我们先看看传统的列表(List)有什么问题。

假设我们要处理一个包含 100 万个数字的序列,每个数字平方后返回。

普通列表做法:

1
2
3
4
5
6
7
8
9
def square_list(n):
result = []
for i in range(n):
result.append(i * i)
return result

# 调用
nums = square_list(1000000)
# ⚠️ 问题:这 100 万个结果必须一次性全部加载到内存中

生成器做法:

1
2
3
4
5
6
7
def square_gen(n):
for i in range(n):
yield i * i

# 调用
nums = square_gen(1000000)
# ✅ 优势:内存中只保存当前计算的那个值,用完即弃

核心区别:

  • 列表:是积极的,一次性把所有结果算好存起来。
  • 生成器:是惰性的(Lazy Evaluation),你问我要一个,我算一个给你。

02 创建生成器的两种方式

1. 生成器函数(Generator Function)

这是最常见的方式。只要函数中包含 yield 关键字,它就不再是普通函数,而是一个生成器工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
def simple_generator():
print("开始执行")
yield 1
print("暂停后恢复")
yield 2
print("即将结束")

gen = simple_generator()
# 注意:这里函数体并没有执行!只是创建了生成器对象

print(next(gen)) # 输出:开始执行 \n 1
print(next(gen)) # 输出:暂停后恢复 \n 2
# print(next(gen)) # 再调用会抛出 StopIteration

关键点: 每次调用 next(),函数从上次 yield 挂起的地方继续执行,直到遇到下一个 yield

2. 生成器表达式(Generator Expression)

类似于列表推导式,但使用圆括号 ()

1
2
3
4
5
6
7
# 列表推导式(立即生成列表)
lst = [x * x for x in range(5)]

# 生成器表达式(惰性生成)
gen = (x * x for x in range(5))

print(type(gen)) # <class 'generator'>

建议: 如果数据量巨大,且只需要遍历一次,优先使用生成器表达式


03 深入底层:yield 到底做了什么?

这是加深理解的关键。yield 不仅仅是“返回值”,它是一个时间机器

  1. 冻结状态:当执行到 yield 时,函数的局部变量指令指针(执行到哪一行)都被冻结保存。
  2. 交出控制权:将 yield 后面的值返回给调用者,函数暂停。
  3. 恢复现场:当下一次 next() 调用时,函数从冻结的地方“解冻”,局部变量的值保持不变,继续向下执行。

图解记忆:
想象你在看电影(函数执行)。

  • return 是看完电影,离场,下次再来得重新买票从头看。
  • yield 是按下暂停键。你可以出去喝杯水(返回数据),回来按播放键(next),剧情接着刚才的地方继续,主角的记忆(局部变量)还在。

04 进阶玩法:双向通信 (send, throw, close)

很多人学到 yield 就停了,其实生成器支持协程(Coroutine) 特性,可以实现双向通信

1. send() 方法

next(gen) 等价于 gen.send(None)。但 send(value) 可以把值发送进生成器内部,成为 yield 表达式的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
def accumulator():
total = 0
while True:
# value 接收外部发送的值,第一次必须是 None
value = yield total
if value is None:
break
total += value

acc = accumulator()
print(next(acc)) # 启动生成器,输出 0
print(acc.send(10)) # 发送 10 进去,total 变为 10,输出 10
print(acc.send(20)) # 发送 20 进去,total 变为 30,输出 30

应用场景: 这种特性常用于构建数据管道状态机

2. throw() 和 close()

  • gen.throw(Exception): 在生成器内部抛出异常,可用于错误处理。
  • gen.close(): 强制关闭生成器,触发 GeneratorExit 异常,常用于清理资源(如关闭文件)。

05 实战场景:哪里该用生成器?

场景 1:读取超大文件

不要一次性 read()readlines()

1
2
3
4
5
6
7
8
def read_large_file(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()

# 内存占用极低,无论文件多大
for line in read_large_file('huge_log.txt'):
process(line)

场景 2:数据流处理管道

将多个生成器串联,形成处理流水线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 读取数据
def get_data():
for i in range(10):
yield i

# 2. 过滤偶数
def filter_even(nums):
for n in nums:
if n % 2 == 0:
yield n

# 3. 平方处理
def square(nums):
for n in nums:
yield n * n

# 管道连接
pipeline = square(filter_even(get_data()))
print(list(pipeline)) # [0, 4, 16, 36, 64]

这种写法既节省内存,又让逻辑清晰解耦。

场景 3:无限序列

生成器不需要知道序列长度。

1
2
3
4
5
6
7
8
9
10
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

fib = fibonacci()
for _ in range(10):
print(next(fib), end=' ')
# 输出:0 1 1 2 3 5 8 13 21 34

06 避坑指南

虽然生成器很强大,但也有几个坑需要注意:

  1. 只能遍历一次:生成器是“消耗品”。一旦遍历结束(抛出 StopIteration),就不能再次使用了。如果需要重用,请重新创建生成器或转为列表(如果内存允许)。
  2. 无法获取长度:生成器没有 __len__ 方法,你不能调用 len(gen)。因为它是惰性的,它自己都不知道后面还有多少个。
  3. 小心 yield 在循环中的变量捕获:如果在循环中创建生成器并引用了循环变量,注意闭包陷阱(虽然这在生成器中不如在普通函数中常见,但仍需留意作用域)。

07 总结

生成器是 Python 中最优雅的特性之一。

  • 核心关键字yield
  • 核心优势:节省内存、支持无限序列、代码解耦。
  • 核心机制:状态保持、惰性求值。
  • 进阶能力send() 实现协程通信。

学习建议:
不要死记硬背。试着把你项目中那个“读取大文件”或者“处理大列表”的函数,改写成生成器版本,感受一下内存的变化。


💬 互动话题:
你在实际项目中用过 send() 方法吗?或者你有用生成器解决过什么棘手问题?欢迎在评论区留言分享!