Python 迭代器与生成器:从协议到实战,一篇讲透
迭代器(Iterator)和生成器(Generator)是 Python 中两个紧密相关但常被混淆的概念。很多人会用 for 循环,但对它们背后的工作机制却模糊不清。
这篇文章从协议规范到内存模型,再到实战场景,把迭代器和生成器彻底讲清楚。
一、迭代器协议:Python 的 for 循环到底怎么工作的?
在 Python 中,可迭代对象(Iterable) 和 迭代器(Iterator) 是两个不同的概念:
-
可迭代对象:实现了
__iter__()方法,返回一个迭代器 -
迭代器:实现了
__iter__()和__next__()两个方法
python
from typing import Iterator, Iterable
class MyRange:
"""自定义可迭代对象"""
def __init__(self, start: int, end: int):
self.start = start
self.end = end
def __iter__(self) -> Iterator[int]:
return MyRangeIterator(self.start, self.end)
class MyRangeIterator:
"""自定义迭代器"""
def __init__(self, current: int, end: int):
self.current = current
self.end = end
def __iter__(self):
return self
def __next__(self) -> int:
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# 使用
for i in MyRange(0, 5):
print(i) # 0, 1, 2, 3, 4
for 循环的背后:
python
# Python 执行 for i in obj 时,实际做了这些事:
iterator = iter(obj) # 调用 obj.__iter__()
while True:
try:
i = next(iterator) # 调用 iterator.__next__()
except StopIteration:
break # 迭代结束
# 执行循环体...
关键理解:
-
可迭代对象是数据容器(如 list、dict、set)
-
迭代器是数据游标(记录当前遍历位置)
-
每次
for循环都会通过iter()获取一个新的迭代器
二、生成器:用函数写迭代器
生成器是用函数语法写迭代器的快捷方式。只要函数里用了 yield,它就变成生成器函数,调用时返回生成器对象(一种特殊的迭代器)。
python
def my_range(start: int, end: int):
"""生成器版本的范围函数"""
current = start
while current < end:
yield current
current += 1
# 调用返回生成器对象
gen = my_range(0, 5)
print(type(gen)) # <class 'generator'>
for i in gen:
print(i) # 0, 1, 2, 3, 4
代码量对比:
| 方案 | 代码行数 | 需要维护的状态 |
|---|---|---|
| 手写迭代器类 | ~15 行 | __iter__、__next__、StopIteration |
| 生成器函数 | ~5 行 | 只需 yield |
生成器的优势:
-
写法简洁:不需要定义类和实现协议方法
-
状态自动保存:
yield会保存当前局部变量、指令指针、调用栈 -
惰性求值:数据按需生成,不一次性加载到内存
三、生成器的执行模型:yield 到底做了什么?
理解 yield 的执行流程是关键:
python
def demo():
print("1. 开始执行")
yield "A"
print("2. yield 后继续")
yield "B"
print("3. 函数结束")
gen = demo()
print(next(gen)) # 打印 "1. 开始执行" → 返回 "A"
print(next(gen)) # 从 yield "A" 后继续 → 打印 "2. yield 后继续" → 返回 "B"
print(next(gen)) # 从 yield "B" 后继续 → 打印 "3. 函数结束" → 抛出 StopIteration
执行流程:
text
调用 demo() → 返回生成器对象(函数体还未执行) 第一次 next(gen) → 执行到 yield "A" → 暂停并返回 "A" 第二次 next(gen) → 从暂停位置继续 → 执行到 yield "B" → 暂停并返回 "B" 第三次 next(gen) → 从暂停位置继续 → 执行完毕 → 抛出 StopIteration
关键理解:yield 既返回值又暂停执行。生成器函数的执行权在调用方和生成器之间交替。
四、生成器的进阶用法:send、throw、close
4.1 send():向生成器发送值
send(value) 可以向生成器传入一个值,这个值会成为当前 yield 表达式的返回值。
python
def accumulator():
total = 0
while True:
value = yield total # yield 返回 total,同时接收外部传入的值
if value is None:
break
total += value
acc = accumulator()
next(acc) # 启动生成器,执行到 yield total,返回 0
print(acc.send(10)) # 传入 10,total=10,执行到 yield total,返回 10
print(acc.send(20)) # 传入 20,total=30,执行到 yield total,返回 30
acc.close() # 关闭生成器
next(gen) 等价于 gen.send(None),所以必须先 next() 启动。
实战场景:协程、数据管道、交互式处理。
4.2 throw():向生成器抛出异常
python
def safe_processor():
try:
while True:
value = yield
print(f"处理: {value}")
except ValueError:
print("收到 ValueError,跳过当前数据")
except GeneratorExit:
print("生成器被关闭")
gen = safe_processor()
next(gen) # 启动
gen.send(10) # 处理: 10
gen.throw(ValueError) # 收到 ValueError,跳过当前数据
gen.send(20) # 处理: 20(生成器继续工作)
gen.close() # 生成器被关闭
4.3 close():提前终止生成器
python
def infinite_sequence():
n = 0
try:
while True:
yield n
n += 1
except GeneratorExit:
print("生成器被关闭,清理资源...")
gen = infinite_sequence()
print(next(gen)) # 0
print(next(gen)) # 1
gen.close() # 生成器被关闭,清理资源...
# next(gen) # 再调用会抛出 StopIteration
最佳实践:如果生成器持有文件句柄、网络连接等资源,用 try/finally 或捕获 GeneratorExit 做清理。
五、生成器表达式:惰性版的列表推导式
python
# 列表推导式:立即计算,占用内存 squares_list = [x**2 for x in range(10000000)] # 立即创建 1000 万个元素的列表 # 生成器表达式:惰性计算,不占内存 squares_gen = (x**2 for x in range(10000000)) # 只返回生成器对象,不计算 # 按需取值 print(next(squares_gen)) # 0 print(next(squares_gen)) # 1
语法区别:
| 写法 | 类型 | 内存占用 | 是否可复用 |
|---|---|---|---|
[x**2 for x in range(10)] |
list | 高 | 是 |
(x**2 for x in range(10)) |
generator | 低 | 否 |
注意:生成器是一次性的。迭代完后再遍历,不会产生任何值:
python
gen = (x for x in range(3)) print(list(gen)) # [0, 1, 2] print(list(gen)) # [] 第二次为空
六、实战场景:生成器解决真实问题
场景一:处理超大日志文件
python
def tail_log(filepath: str, keyword: str):
"""实时跟踪日志文件,过滤包含关键词的行"""
with open(filepath, "r", encoding="utf-8") as f:
# 跳到文件末尾
f.seek(0, 2)
while True:
line = f.readline()
if not line:
import time
time.sleep(0.1)
continue
if keyword in line:
yield line.strip()
# 使用
for line in tail_log("/var/log/app.log", "ERROR"):
print(f"发现异常: {line}")
if "FATAL" in line:
break # 可随时停止
场景二:分页拉取 API 数据(带限流)
python
import time
import requests
from typing import Generator, Dict, Any
def paginated_api(base_url: str, page_size: int = 50, rate_limit: float = 1.0) -> Generator[Dict[str, Any], None, None]:
"""分页拉取 API 数据,支持限流"""
page = 1
while True:
resp = requests.get(
base_url,
params={"page": page, "size": page_size},
timeout=10
)
resp.raise_for_status()
data = resp.json()
if not data.get("items"):
break
for item in data["items"]:
yield item
# 检查是否最后一页
if page >= data.get("total_pages", float("inf")):
break
page += 1
time.sleep(rate_limit) # 限流
# 使用方完全不用关心页码和限流
for user in paginated_api("https://api.example.com/users"):
print(user["name"])
if user["id"] == 1000:
break
场景三:管道式数据处理
python
def read_numbers(n: int):
for i in range(n):
yield i
def even_only(source):
for num in source:
if num % 2 == 0:
yield num
def square(source):
for num in source:
yield num * num
def take(source, n: int):
count = 0
for item in source:
if count >= n:
break
yield item
count += 1
# 链式组合:取 100 以内偶数的平方(取前 5 个)
pipeline = take(square(even_only(read_numbers(100))), 5)
print(list(pipeline)) # [0, 4, 16, 36, 64]
每个环节只做一件事,数据惰性流动,没有中间列表产生。
七、迭代器 vs 生成器:一句话对比
| 对比维度 | 迭代器 | 生成器 |
|---|---|---|
| 定义方式 | 实现 __iter__ + __next__ |
函数中有 yield |
| 代码量 | 较多(需定义类) | 较少(函数即可) |
| 状态管理 | 手动维护实例变量 | 自动保存局部变量 |
| 可复用性 | 可多次迭代(重新创建实例) | 一次性 |
| 双向通信 | 不支持 | 支持 send()/throw() |
| 适用场景 | 需要封装复杂迭代逻辑 | 大部分迭代场景 |
八、记忆口诀
迭代器是协议,生成器是语法糖;
yield 暂停又返回,next 驱动向前闯;
惰性求值省内存,管道组合威力强。
总结
-
迭代器是实现了
__iter__和__next__的对象,是for循环的基础 -
生成器是用
yield写迭代器的快捷方式,自动保存状态 -
生成器表达式是惰性版的列表推导式,节省内存
-
send()、throw()、close()是生成器的进阶控制能力 -
处理大文件、流式数据、管道组合时,优先考虑生成器
本文为纯技术分享,不涉及任何品牌或产品。
更多推荐


所有评论(0)