Python 迭代器与生成器

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Python 3.13

生成器让你「边算边给」,不必先建一个完整列表。处理大文件、流数据、无限序列时靠它。

环境准备

示例在 Python 3.13 下验证,标准库即可。

可迭代对象与迭代器

  • 可迭代对象(iterable):能被 for 遍历,实现 __iter__。列表、字符串、字典、集合、文件对象都是。
  • 迭代器(iterator):实现 __iter__ 和 __next__,记住当前位置,只能前进。
items = [1, 2, 3]
it = iter(items)      # 拿到迭代器
next(it)              # 1
next(it)              # 2
next(it)              # 3
next(it)              # StopIteration 异常

for 循环做的事就是:拿迭代器 → 反复 next() → 捕获 StopIteration。

for x in items:
    print(x)
# 等价于
it = iter(items)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    print(x)

列表可以反复遍历(每次 iter() 返回新迭代器),迭代器本身只能用一次。

生成器函数:yield

函数里有 yield,调用它不再执行函数体,而是返回一个生成器对象:

def count_up(n: int):
    for i in range(n):
        print(f"产出 {i}")
        yield i

gen = count_up(3)     # 没输出,函数体还没执行
next(gen)             # 产出 0 → 返回 0
next(gen)             # 产出 1 → 返回 1
list(gen)             # 产出 2 → [2]

执行流程:每次 next() 走到 yield 就暂停并保留局部变量,下次从暂停处继续;函数结束(或 return)时抛 StopIteration。

这就是「惰性」的来源——值在需要时才计算。

为什么用生成器

对比读一个大文件:

# 一次读入:文件 2GB 就占 2GB 内存
lines = Path("huge.log").read_text(encoding="utf-8").splitlines()
errors = [line for line in lines if "ERROR" in line]

# 生成器:内存中只有当前一行
def read_lines(path: str):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

errors = (line for line in read_lines("huge.log") if "ERROR" in line)
for line in errors:
    print(line)

生成器表达式 (...) 与生成器函数可以串成管道,逐条处理,内存恒定。

yield from

把「遍历另一个可迭代对象并逐个产出」简写成一行,还能转发 send:

def chain(*iterables):
    for it in iterables:
        yield from it        # 等价于 for x in it: yield x

list(chain([1, 2], [3]))     # [1, 2, 3]

递归遍历目录树很自然:

def walk(path: Path):
    for child in path.iterdir():
        if child.is_dir():
            yield from walk(child)
        else:
            yield child

send、throw 与 close

生成器可以双向通信:

def accumulator():
    total = 0
    while True:
        value = yield total      # yield 表达式的值来自 send
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)          # 启动,产出 0(必须先启动)
acc.send(10)       # 10
acc.send(5)        # 15
acc.close()
  • gen.send(value):把值送进生成器,作为 yield 表达式的结果。
  • gen.throw(Exc):在暂停处抛出异常。
  • gen.close():结束生成器(内部抛 GeneratorExit)。

日常代码很少用 send,它主要出现在协程与框架的底层。

自己写迭代器

需要复用、可多次遍历、或要额外方法时,写成类:

class Range2:
    def __init__(self, stop: int):
        self.stop = stop
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

常见错误与注意事项

1. 生成器只能用一次

gen = (x * 2 for x in [1, 2])
list(gen)     # [2, 4]
list(gen)     # [],已耗尽

要多次遍历就写成函数,每次调用返回新生成器。

2. 把 return 的值当成产出

生成器函数里的 return value 不会产出值,值会放进 StopIteration.value(通常由框架取出)。要产出就用 yield。

3. 生成器里的异常被延迟

函数体只在迭代时执行,所以 open 失败之类的错误会在第一次 next() 时才抛,栈看起来离定义处很远。调试时注意。

4. 生成器没关闭导致资源泄漏

生成器里打开的文件、连接,如果没迭代完就不会执行到清理代码。用 with contextlib.closing(gen) 或确保迭代完整。

5. 过度使用生成器

数据量小时列表更快也更直观,惰性只在数据大或无限时才有意义。

小结

  • 可迭代对象能反复遍历,迭代器只能前进一次。
  • yield 让函数在产出时暂停,保留状态,实现惰性计算。
  • 用生成器管道处理大文件,内存只占一行。
  • yield from 委托给另一个可迭代对象;send 用于双向通信(高级场景)。

下一步