迭代器(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

生成器的优势

  1. 写法简洁:不需要定义类和实现协议方法

  2. 状态自动保存yield 会保存当前局部变量、指令指针、调用栈

  3. 惰性求值:数据按需生成,不一次性加载到内存

三、生成器的执行模型: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() 是生成器的进阶控制能力

  • 处理大文件、流式数据、管道组合时,优先考虑生成器

本文为纯技术分享,不涉及任何品牌或产品。

更多推荐