返回专题首页

Python 专题

惰性计算的力量:迭代器、生成器与 yield 详解

如果说列表和字典解决的是“怎么存数据”,那么迭代器和生成器解决的就是“怎么流动地处理数据”。它们看起来比普通容器更抽象一些,但一旦理解了惰性计算的好处,就会发现这套机制在性能和表达力上都非常强大。

Python 专题第 13 篇 / 39 篇5 分钟

如果说列表和字典解决的是“怎么存数据”,那么迭代器和生成器解决的就是“怎么流动地处理数据”。它们看起来比普通容器更抽象一些,但一旦理解了惰性计算的好处,就会发现这套机制在性能和表达力上都非常强大。

这一节我们会从 iter()next() 讲起,再过渡到 yield、生成器表达式和实际使用场景,让你真正明白为什么 Python 社区会如此偏爱这类写法。

从可迭代对象开始理解

很多人第一次接触迭代器时,很容易把“可迭代对象”和“迭代器”当成一回事。它们确实很接近,但并不完全相同。

像列表、元组、字符串、字典这些能被 for 遍历的对象,通常都属于可迭代对象。也就是说,它们能够提供一个“把元素一个个拿出来”的能力。

例如:

items = ["python", "fastapi", "sqlalchemy"]

for item in items:
    print(item)

这里的 items 是可迭代对象,但它本身并不一定就是迭代器。

你可以先建立一个简单心智:

  • 可迭代对象负责“提供遍历能力”;
  • 迭代器负责“记住当前遍历到哪里”。

这个区别虽然抽象,但一旦想通,你就会更清楚 for 循环背后到底发生了什么。

迭代器协议:iter()next()

Python 的迭代机制建立在一套很简单的协议上。

先通过 iter() 从可迭代对象中拿到迭代器:

items = [1, 2, 3]
iterator = iter(items)

然后通过 next() 逐个取值:

print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3

当元素取完后,再继续调用 next(),就会抛出 StopIteration

这其实就是 for 循环在底层做的事情。也就是说,for item in items 并不是某种神秘语法糖,而是解释器不断从迭代器里取下一个元素,直到结束。

理解这件事的意义在于:你会意识到 Python 在处理数据流时,并不总是一次性把所有结果都准备好,而是允许它们按需逐步产生。

生成器函数与 yield

生成器是构造迭代器最自然的一种方式。它最显著的特征,就是函数体里出现了 yield

def count_up_to(limit):
    current = 1
    while current <= limit:
        yield current
        current += 1

调用这个函数时,它不会像普通函数那样一次性执行到底,而是返回一个生成器对象。每次迭代时,才运行到下一个 yield

生成器和普通函数的区别

普通函数的心智是“调用一次,执行到底,返回一个结果”。

生成器函数的心智则是“调用后得到一个可迭代的数据流,每次需要时再往前推进一点”。

这意味着生成器特别适合以下场景:

  • 结果很多,没必要一次性全部放进内存;
  • 数据是逐步产生的;
  • 处理链条天然可以按阶段一段段推进。

所以,yield 的本质不是“返回值的另一种写法”,而是把函数从“一次性计算器”变成了“逐步产出结果的迭代器工厂”。

生成器表达式

除了生成器函数,Python 还有生成器表达式:

numbers = (num * 2 for num in range(10))

它和列表推导式长得很像,只不过外层用的不是 [],而是 ()

两者最关键的区别在于:

  • 列表推导式会立刻生成完整列表;
  • 生成器表达式只会在需要时逐步计算。

这使得生成器表达式在大数据量场景下会更加节省内存,也更适合和 sum()any()all() 这类函数配合使用。

惰性计算为什么重要?

所谓惰性计算,可以简单理解成:不急着把所有结果算出来,而是在真正需要时再计算。

这带来的好处非常直接。

第一,是节省内存。如果你要处理一个几百万行的大文件,一次性把所有数据读进列表里,压力会很大;但如果是一行一行地产出、处理、丢弃,内存占用就会稳定很多。

第二,是提升表达能力。很多处理流程天然就是“输入一批,输出一批”,比如过滤、转换、分页拉取、日志流处理。生成器让这类管道式写法变得非常自然。

第三,是提升性能上的灵活性。惰性计算不一定总是更快,但它能避免做“不必要的工作”。如果你只是想找到第一个满足条件的结果,就没必要先把所有候选结果全算完。

所以,惰性计算的核心价值不是“高级”,而是让计算成本和真实需求尽量对齐。

典型应用场景:大文件、流式处理与分页抓取

生成器最典型的使用场景之一,就是大文件处理。

例如:

def read_lines(path):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()

这样调用者就可以按行消费,而不是一次性读完整个文件。

另一个很自然的场景,是流式数据处理。例如一批数据先过滤、再转换、再聚合,这时每一步都可以用生成器串成管道。

分页抓取接口时也很常见。你不一定想一次性把所有页面都请求完,而是边请求边处理:

def fetch_all_pages(client):
    page = 1
    while True:
        items = client.fetch(page=page)
        if not items:
            break
        yield from items
        page += 1

这种写法比“先拉全量列表再统一处理”更贴近真实场景,也更利于控制资源消耗。

总结与预告

这一节我们理解了迭代器与生成器为什么不仅仅是“高级写法”,而是 Python 在处理大规模数据和流式任务时非常关键的一套机制。掌握惰性计算以后,你对内存、性能和表达方式都会有新的感觉。

下一节我们会继续进入 Python 很有代表性的高级特性,从闭包一路走到装饰器,把 @ 背后的本质真正讲明白。