RSS
菜单
全部文章快讯开发科技深度热点

迭代器、生成器与协程(Python 从精通到入门 · 05)

内容摘要

for x in y 背后只有三个约定。生成器把它们包装成语法糖,协程又在生成器上长出来——三者是同一条演化链。

for x in y 背后只有三个约定。生成器把它们包装成语法糖,协程又在生成器上长出来——三者是同一条演化链。

你将学到

  • 可迭代对象与迭代器的区别,以及 for 循环到底做了什么
  • 生成器函数、yield、send / throw / close、yield from
  • 生成器表达式与惰性求值
  • 生成器如何一步步演化成协程(asyncio 的前夜)
  • 用生成器处理大文件与无限序列

前置知识

04 - 上下文管理器与 with。本篇是大师篇的收尾,下一篇进入并发世界。


一、可迭代对象 vs 迭代器

两句话分清:

  • 可迭代对象(iterable):实现了 __iter__,能产出迭代器;
  • 迭代器(iterator):实现了 __next__,能一次吐一个值,并在耗尽后抛 StopIteration。
nums = [1, 2, 3]

print(hasattr(nums, "__iter__"))   # 输出: True   —— 列表是可迭代对象
print(hasattr(nums, "__next__"))   # 输出: False  —— 但列表不是迭代器
it = iter(nums)                    # 用 iter() 取出迭代器
print(next(it), next(it))          # 输出: 1 2
print(iter(it) is it)              # 输出: True   —— 迭代器的 __iter__ 返回自己

分清这一点很实用:迭代器只能走一遍。列表、元组、字典、文件对象、生成器都可迭代,但只有迭代器是「一次性」的。

二、迭代器协议:__iter__ / __next__ / StopIteration

for 循环的展开就是「先 iter(),再反复 next(),捕获 StopIteration」:

class Countdown:
    def __init__(self, start): self.n = start
    def __iter__(self):
        return self                      # 自己就是迭代器
    def __next__(self):
        if self.n <= 0:
            raise StopIteration          # 用异常表示「没有了」
        self.n -= 1
        return self.n + 1

for x in Countdown(3):
    print(x)
# 输出: 3 / 2 / 1

等价的 while 写法(这就是 for 做的事):

it = iter(Countdown(3))
while True:
    try:
        print(next(it))                  # for 循环内部就是这么干的
    except StopIteration:                # 异常被 for 悄悄接住
        break

自己写迭代器类很啰嗦——这正是生成器要解决的痛点。

三、生成器函数与 yield

函数体里出现 yield,它就不再是普通函数,而是生成器函数:调用它只会得到生成器对象,函数体一行都不执行:

def countdown(n):
    print("开始")
    while n > 0:
        yield n                          # 产出一个值并暂停在这里
        n -= 1
    print("结束")

g = countdown(3)                         # g 是 <generator object ...>,函数体一行未执行
print("还没开始")                        # 输出: 还没开始
print(next(g))                           # 输出: 开始 / 3
print(list(g))                           # 输出: 先打印「结束」,再打印 [2, 1]

关键性质:惰性(没有 next() 就不计算)、状态自动保存(局部变量冻结在栈帧里)、它就是迭代器(自带 __iter__ / __next__,能直接进 for)。

四、send / throw / close

yield 是个「双向通道」:它既能把值送出去,也能接收值进来。

def echo():
    while True:
        x = yield                        # yield 表达式的结果 = send 进来的值
        print(f"收到 {x}")

g = echo()
next(g)              # 先启动,跑到第一个 yield 处挂起
g.send("hello")      # 输出: 收到 hello
g.send(42)           # 输出: 收到 42
g.close()            # 通知生成器结束

yield 还能出现在赋值右边,用来「产出上一个结果、接收下一个输入」:

def acc():
    total = 0
    while True:
        x = yield total                  # 先返回 total,再等下一个输入
        if x is None:
            break
        total += x

g = acc()
print(next(g))       # 输出: 0
print(g.send(10))    # 输出: 10
print(g.send(5))     # 输出: 15

throw 把异常「扔进」生成器(在挂起的 yield 处抛出),close 则在生成器内部触发 GeneratorExit:

def worker():
    while True:
        try:
            yield                        # 挂起在这里等异常
        except ValueError as e:
            print(f"内部捕获: {e}")        # 接住后继续循环,生成器不结束

g = worker()
next(g)
g.throw(ValueError("糟糕"))     # 输出: 内部捕获: 糟糕
g.close()

send / throw 是 PEP 342 引入的——它们把生成器从「数据生产者」升级成「可双向对话的协程」,这是后半段的伏笔。

五、yield from:把迭代委托出去

想从一个生成器里「转发」另一个可迭代对象,yield from 比手写 for 更短,而且完整透传 send / throw / close,还能接收子生成器的返回值:

def chain(*iterables):
    for it in iterables:
        yield from it                    # 等价于 for x in it: yield x,但更强
print(list(chain([1, 2], "ab", (3, 4))))
# 输出: [1, 2, 'a', 'b', 3, 4]

委托的「返回值」会直接成为 yield from 表达式的值:

def inner():
    total = 0
    while True:
        x = yield
        if x is None:
            return total                 # 用 return 把结果交回给委托方
        total += x
def outer():
    result = yield from inner()          # 拿到 inner 的返回值
    print("inner 返回", result)
    yield "结束"

g = outer()
next(g)
g.send(1)
g.send(2)
print(g.send(None))                      # 输出: inner 返回 3,然后 结束

没有 yield from 的时代,这类「协程嵌套」得手写一堆 send/throw 转发代码。asyncio 的早期实现就是靠它驱动的。

六、生成器表达式

把列表推导式的方括号换成圆括号,得到的就是生成器——惰性、不占内存:

gen = (x * x for x in range(5))
print(gen)                              # 输出: <generator object <genexpr> at 0x...>
print(sum(gen))                         # 输出: 30
print(sum(x * x for x in range(5)))     # 输出: 30(函数唯一参数时可省掉一层括号)

[x*x for x in range(10**7)] 会立刻造出上千万元素的列表;(...) 版本的内存占用几乎只和「当前这一个元素」有关。

七、生成器如何演化为协程(asyncio 前夜)

把生成器当协程用,只需要一个「调度器」轮流驱动它们:

def coro_a():
    print("A: 开始")
    yield                                # 让出控制权
    print("A: 恢复")
def coro_b():
    print("B: 开始")
    yield
    print("B: 恢复")

tasks = [coro_a(), coro_b()]
while tasks:                             # 一个最朴素的「事件循环」
    task = tasks.pop(0)
    try:
        next(task)
        tasks.append(task)               # 没结束就放回队列尾部
    except StopIteration:
        pass                             # 结束的就不再入队

# 输出: A: 开始 / B: 开始 / A: 恢复 / B: 恢复

这段 20 行不到的东西,就是 asyncio 调度器的骨架。真实的 asyncio 在此之上加了三样东西:事件循环(loop)、Future/Task 封装、以及基于 select 的 IO 多路复用。演化链条是:

生成器函数 (yield) → 可双向通信 (PEP 342) → 可委托嵌套 (PEP 380) → 原生协程 (PEP 492)

到 PEP 492 之后,协程和生成器在语言层面彻底分家:

import asyncio

async def native():
    return 42

c = native()
print(type(c))                 # 输出: <class 'coroutine'>
# next(c)                      # TypeError: 'coroutine' object is not an iterator
c.close()                      # 不这样处理会报 "coroutine was never awaited" 警告
print(asyncio.run(native()))   # 输出: 42

原生协程不能再 next(),必须由事件循环驱动。细节留给 06 - GIL 真相与并发模型选择 与后面的 asyncio 专篇。

八、实战:大文件与无限序列

按块读大文件——无论文件多大,内存占用都是常量:

def read_in_chunks(path, size=8192):
    with open(path, "rb") as f:
        while chunk := f.read(size):     # 海象运算符,3.8+
            yield chunk                  # 出一块,读一块

无限序列也能安全使用,因为有「惰性」兜底:

from itertools import islice
def fib():
    a, b = 0, 1
    while True:                          # 不设终点,靠调用方控制取多少
        yield a
        a, b = b, a + b

print(list(islice(fib(), 10)))
# 输出: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

itertools 里全是生成器思路的产物:islice、chain、count、groupby、takewhile——它们都只在你消费时才计算。

常见坑

坑 1:生成器只能遍历一次。

# ❌ 第二次遍历拿到空列表
g = (x for x in range(3))
print(list(g))     # 输出: [0, 1, 2]
print(list(g))     # 输出: []

# ✅ 需要多次遍历就存成列表 / 元组;只看一次才用生成器省内存

坑 2:想用 return 给 for 循环传值。

# ❌ 循环只会打印 1,那个 2 拿不到
def g():
    yield 1
    return 2
for x in g():
    print(x)      # 只打印 1,那个 2 拿不到

# ✅ 想拿到返回值,用 `yield from` 接收(见第五节)

坑 3:以为生成器表达式会立刻执行。

# ❌ 打印发生在「消费」时,不是创建时
g = (print(x) for x in range(3))
print("还没打印")     # 输出: 还没打印
list(g)              # 此时才输出: 0 / 1 / 2

# ✅ 需要立刻执行就用列表推导式 [print(x) for x in range(3)]

小结

  • 可迭代对象实现 __iter__,迭代器实现 __next__;迭代器是一次性的,for = iter() + 反复 next() + 接住 StopIteration。
  • 含 yield 的函数返回生成器;调用时不执行任何代码,直到第一次 next()。
  • send / throw / close 让生成器变成双向通道;yield from 负责透明委托。
  • 生成器表达式惰性求值,内存友好;适合大文件与无限序列。
  • 演化链:yield → send/throw → yield from → async/await。asyncio 的调度器就诞生在这条链的尽头。

延伸阅读

  • Python 官方文档 Iterators 与 itertools
  • 《Fluent Python》第 17 章「Generators」;PEP 342 / 380 / 492 连着读,演化脉络一目了然
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《迭代器、生成器与协程(Python 从精通到入门 · 05)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。