深色模式
02 · 迭代器、生成器与 yield:要一个给一个
学完你能说出:
- 可迭代对象、迭代器、生成器三者的关系。
yield做了什么:暂停函数、交出一个值、下次从暂停处继续。- 生成器为什么省内存,适合哪些场景(大文件、流式输出)。
先看地图
Python 模块第二篇。AI 应用里经常要处理大文件、大批数据,还有大模型的流式输出(一个字一个字地返回),背后都是这一篇的知识。
本篇讲 4 个点:① for 循环背后发生了什么 → ② 迭代器 → ③ 生成器和 yield → ④ 什么时候用。
一句话本质
生成器 = 「要一个,算一个」的数据来源。不提前把所有数据算好放进内存,而是每次被要的时候才生成下一个。
生活类比:自助餐 vs 现点现做
- 列表 = 自助餐:所有菜提前做好,摆满一桌(全部放进内存)。菜多了,桌子摆不下。
- 生成器 = 现点现做:你要一道,厨师做一道。永远只占一个盘子的位置。
- 代价:现点现做的菜只能按顺序吃一遍,不能回头再拿第一道(生成器只能遍历一次)。
一步步看
1. for 循环背后发生了什么
python
for x in [10, 20, 30]:
print(x)Python 实际做的事:
python
it = iter([10, 20, 30]) # iter = iterator 迭代器:先拿到一个迭代器
next(it) # → 10 # next = 下一个
next(it) # → 20
next(it) # → 30
next(it) # → 抛出 StopIteration(stop 停止 + iteration 迭代),for 循环据此结束- 可迭代对象(Iterable):能被
for遍历的东西,比如列表、字符串、字典。它能通过iter()产出一个迭代器。 - 迭代器(Iterator):一个「记得自己走到哪了」的游标,每次
next()给出下一个值。
2. 生成器:用 yield 写迭代器
python
def count_up(n): # 定义一个生成器函数
i = 1
while i <= n:
yield i # yield = 产出:交出 i,然后在这里暂停
i += 1
g = count_up(3) # 这时函数体一行都还没执行
next(g) # → 1 # 运行到第一个 yield,交出 1,暂停
next(g) # → 2 # 从暂停处继续,i 变成 2,再交出
next(g) # → 3
next(g) # → StopIterationyield 和 return 的区别:return 交出值后函数就结束了;yield 交出值后函数只是暂停,局部变量都还在,下次接着往下跑。
3. 省多少内存(数字是示意)
python
nums = [x * x for x in range(10_000_000)] # 列表:1000 万个数全在内存,几百 MB
nums = (x * x for x in range(10_000_000)) # 生成器表达式:只占一两百字节只是把 [] 换成 (),内存占用天差地别。
4. 读大文件
python
def read_lines(path):
with open(path, encoding="utf-8") as f: # with 语句:用完自动关闭文件
for line in f: # 文件对象本身就是迭代器,一次读一行
yield line.strip() # strip = 去掉首尾空白
for line in read_lines("10GB日志.txt"): # 10GB 的文件,内存里始终只有一行
...为什么这么设计
为什么要有迭代器协议?
- 问题:列表、字典、文件、网络流的结构完全不同,如果
for要为每种类型单独写一套逻辑,会非常乱。 - 方案:统一约定:只要实现了
__iter__和__next__,就能被for遍历。 - 好处:
for、sum()、list()等工具对所有数据源通用;自己写的类也能直接用在for里。
一句话答法
迭代器协议给所有「能逐个取值」的对象定了统一接口,for 循环和各种内置函数因此能通用于列表、文件、生成器等任何数据源。
为什么要有生成器?
- 问题:数据量大(大文件、海量记录)或无限(实时数据流)时,提前全部生成会占满内存,甚至根本做不到。
- 方案:用
yield惰性生成(用到时才算),每次只产出一个值。 - 好处:内存占用和数据总量无关;而且第一个结果马上就能拿到,不用等全部算完。
一句话答法
生成器按需惰性产出数据,内存占用与数据总量无关,适合处理大文件、大数据和数据流,还能更快拿到第一个结果。
英文术语拆解
| 术语 | 拆开看 | 一句话意思 |
|---|---|---|
| Iterable | iterate 迭代 + -able 能…的 | 可迭代对象:能被 for 遍历 |
| Iterator | iterate + -or 「做…的东西」 | 迭代器:记得进度、能逐个给值 |
| Generator | generate 生成 + -or | 生成器:用 yield 写的迭代器 |
| yield | 产出、让出 | 交出一个值并暂停函数 |
| StopIteration | stop 停止 + iteration 迭代 | 迭代结束的信号 |
| Lazy Evaluation | lazy 懒 + evaluation 求值 | 惰性求值:用到时才计算 |
__iter__ / __next__ | 前后双下划线的「魔法方法」 | 迭代器协议要实现的两个方法 |
面试怎么问
Q:迭代器和生成器的区别?
迭代器是实现了
__iter__和__next__的对象;生成器是用yield(或生成器表达式)写出来的一种迭代器,Python 自动帮你实现了这两个方法并保存执行状态,写起来更简洁。所有生成器都是迭代器,反过来不一定。
Q:yield 和 return 的区别?
return 返回值并结束函数;yield 产出一个值后暂停函数,保留局部变量和执行位置,下次调用 next 时从暂停处继续。含有 yield 的函数调用后返回的是生成器对象,不会立即执行函数体。
Q:生成器有什么应用场景?
处理大文件或大数据集(逐行、逐批处理);数据管道(一层生成器套一层,每层做一种处理);无限序列;流式输出,比如逐块返回大模型的生成结果。
容易混的地方
- 生成器只能遍历一次:遍历完就耗尽了,再
for一遍什么都没有。需要多次使用就重新创建,或者转成列表。 [x for x in ...]是列表,(x for x in ...)是生成器:括号不同,行为完全不同。- 调用生成器函数不会执行函数体:
g = count_up(3)只是创建了生成器,第一次next()时才开始执行。
和其他知识的连接
- ← 来自 GIL 与多线程:同属「Python 怎么高效处理数据」。
- → 影响 async/await 异步编程:协程的「暂停 → 继续」最早就是在生成器的基础上发展出来的。
- → 影响 大模型在做什么:大模型逐个 Token 流式返回,后端常用生成器把结果一段一段推给前端。
自测 3 问
Q1:用「自助餐 vs 现点现做」类比说明列表和生成器的区别。
点开看答案
列表像自助餐,所有数据提前生成、全部放进内存,数据多了放不下;生成器像现点现做,要一个才生成一个,内存里始终只有当前这个。代价是生成器只能按顺序遍历一次。
Q2:yield 和 return 有什么区别?
点开看答案
return 交出值后函数结束;yield 交出值后函数暂停,局部变量和执行位置都保留,下次 next() 时从暂停处继续执行。
Q3:要逐行处理一个 10GB 的日志文件,怎么写才不会占满内存?
点开看答案
用 with open(...) as f: for line in f: 逐行读取(文件对象本身就是迭代器),或者包装成生成器函数,每次 yield 一行。不要用 f.read() 或 f.readlines() 一次读进内存。
✅ 验收清单
- 能说出 for 循环背后的
iter()→next()→StopIteration过程 - 能区分可迭代对象、迭代器、生成器
- 能手写一个用
yield的生成器函数,并说出每次next()时发生了什么 - 能讲清生成器为什么省内存(问题 → 方案 → 好处)
- 能说出生成器的 3 个应用场景,以及「只能遍历一次」这个坑
下一步该懂什么
- 想给一个函数统一加上「计时」「重试」「打日志」,又不想改它的代码?→ 装饰器与闭包