Skip to content

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)  # → StopIteration

yield 和 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 惰性生成(用到时才算),每次只产出一个值。
  • 好处:内存占用和数据总量无关;而且第一个结果马上就能拿到,不用等全部算完。

一句话答法

生成器按需惰性产出数据,内存占用与数据总量无关,适合处理大文件、大数据和数据流,还能更快拿到第一个结果。

英文术语拆解 ​

术语拆开看一句话意思
Iterableiterate 迭代 + -able 能…的可迭代对象:能被 for 遍历
Iteratoriterate + -or 「做…的东西」迭代器:记得进度、能逐个给值
Generatorgenerate 生成 + -or生成器:用 yield 写的迭代器
yield产出、让出交出一个值并暂停函数
StopIterationstop 停止 + iteration 迭代迭代结束的信号
Lazy Evaluationlazy 懒 + 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 个应用场景,以及「只能遍历一次」这个坑

下一步该懂什么 ​

  • 想给一个函数统一加上「计时」「重试」「打日志」,又不想改它的代码?→ 装饰器与闭包