1. 生成器表达式基础:从 yield 到 send 的进化之路
说实话,我刚开始接触 Python 生成器的时候,完全没意识到这东西有多强大。那时候我还在用列表推导式一把梭,直到有一次处理一个 500MB 的日志文件,内存直接爆了。嗯,从那以后,生成器就成了我的标配。
这一章我们不讲花哨的,先把地基打牢。你想想看,生成器到底是什么?说白了,它就是一种能记住执行状态的函数。每次调用它,不是从头开始,而是从上次停下的地方继续跑。
1.1 yield 关键字:暂停与恢复的魔法
yield 是生成器的灵魂。它跟 return 有点像,都会返回一个值。但区别在于:return 是「一去不复返」,而 yield 是「我还会回来的」。
核心区别:
return→ 函数结束,释放所有局部变量yield→ 函数暂停,保留当前状态(局部变量、指令指针等)
来看个最简单的例子:
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
# 使用生成器
counter = count_up_to(3)
print(next(counter)) # 输出: 1
print(next(counter)) # 输出: 2
print(next(counter)) # 输出: 3
# 再调一次就会抛 StopIteration
我在项目中遇到过不少新手把 yield 和 return 混着用。记住一条铁律:一个函数里只要出现 yield,它就不再是普通函数了,而是生成器函数。这时候你写 return value 其实是在触发 StopIteration(value),不是真的返回。
1.2 生成器函数定义:两种写法,一个本质
Python 里定义生成器有两种方式。我个人习惯把第一种叫「显式派」,第二种叫「语法糖派」。
| 方式 | 写法 | 适用场景 |
|---|---|---|
| 生成器函数 | def gen(): yield x |
逻辑复杂,需要多步处理 |
| 生成器表达式 | (x for x in iterable) |
逻辑简单,一行搞定 |
生成器表达式其实就是把列表推导式的方括号 [] 换成圆括号 ()。但别小看这个变化,内存占用天差地别。
# 列表推导式:一次性生成所有元素
list_comp = [x**2 for x in range(1000000)] # 占用 ~28MB 内存
# 生成器表达式:按需生成,一次一个
gen_expr = (x**2 for x in range(1000000)) # 占用 ~56 字节内存
我曾经踩过的坑: 有一次在 API 接口里用列表推导式处理用户上传的 CSV 数据,文件不大也就几万行,但并发一上来,服务器直接 OOM。换成生成器表达式后,内存从 200MB 降到了 2MB。别笑,真事。
1.3 next() 与 send():单向 vs 双向通信
next() 是单向的——你从生成器拿数据。但 send() 不一样,它允许你往生成器里传值。这就变成了双向通信。
为什么会需要 send()?我举个例子:假设你在写一个数据校验流水线,每一步都需要根据上一步的结果动态调整参数。这时候 send() 就派上用场了。
def echo_generator():
print("生成器启动...")
while True:
received = yield # yield 右侧没有表达式,只接收值
print(f"收到: {received}")
gen = echo_generator()
next(gen) # 启动生成器,执行到第一个 yield
gen.send("Hello") # 输出: 收到: Hello
gen.send("World") # 输出: 收到: World
注意看,第一次调用必须用 next(gen) 或者 gen.send(None)。为什么?因为生成器还没跑到 yield 那里,你没法给它传值。这个细节我当年调试了半小时才搞明白。
小技巧: 如果你想让生成器既能产出值又能接收值,可以写成 received = yield output。这样 next() 返回 output,send(value) 把 value 赋给 received。
1.4 生成器 vs 列表推导式:不只是括号的区别
很多人觉得生成器表达式就是「省内存的列表推导式」。嗯,对了一半。它们之间的差异远不止内存。
| 对比维度 | 列表推导式 | 生成器表达式 |
|---|---|---|
| 计算时机 | 立即计算(饥渴求值) | 延迟计算(惰性求值) |
| 内存占用 | O(n),全部元素在内存 | O(1),一次只存一个元素 |
| 可迭代次数 | 无限次(可重复遍历) | 一次(遍历完就耗尽) |
| 支持索引 | 支持 list[3] |
不支持 |
| 支持 len() | 支持 | 不支持 |
你想想看,什么时候该用哪个?我个人的经验法则:
- 数据量小且需要多次遍历 → 列表推导式
- 数据量大或只需遍历一次 → 生成器表达式
- 需要随机访问(索引) → 列表推导式
- 做流水线处理(链式生成器) → 生成器表达式
# 错误示范:生成器只能遍历一次
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] — 已经空了!
# 正确做法:需要多次遍历就用列表
lst = [x for x in range(5)]
print(lst) # [0, 1, 2, 3, 4]
print(lst) # [0, 1, 2, 3, 4]
我曾经犯过的错: 写了一个生成器处理日志流,然后在代码里遍历了两次。第二次遍历啥也没拿到,排查了半天才发现生成器已经耗尽了。从那以后,我养成了一个习惯:如果同一个数据集需要遍历超过一次,直接转成列表。
1.5 本章知识体系总览
下面这张图把生成器表达式的核心概念串起来了。你可以把它当作本章的「地图」,后面深入学习时随时回来对照。
这张图把本章的四个核心知识点串在了一起。你可以看到,yield 是生成器的底层机制,生成器函数是它的载体,next()/send() 是它的驱动方式,而和列表推导式的对比则帮你选对工具。
好了,第一章就到这里。记住:生成器不是银弹,但在处理大数据流、无限序列、流水线处理这些场景下,它绝对是你工具箱里最趁手的家伙。后面我们会一步步深入,把它的高级玩法全挖出来。