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() vs 列表推导式 暂停 / 恢复状态 保留局部变量 def + yield 定义 返回生成器对象 next() 单向拉取 send() 双向通信 惰性求值 vs 饥渴求值 O(1) vs O(n) 内存 核心原则 生成器 = 惰性求值 + 状态保留 + 按需生成

这张图把本章的四个核心知识点串在了一起。你可以看到,yield 是生成器的底层机制,生成器函数是它的载体,next()/send() 是它的驱动方式,而和列表推导式的对比则帮你选对工具。

好了,第一章就到这里。记住:生成器不是银弹,但在处理大数据流、无限序列、流水线处理这些场景下,它绝对是你工具箱里最趁手的家伙。后面我们会一步步深入,把它的高级玩法全挖出来。