Python 生成器与迭代器:惰性求值与流式处理

FreeGuideOnline 最新 2026-06-16

理解迭代:从for循环到迭代器协议

你是否好奇过,为什么for循环可以遍历列表、字符串,甚至文件对象?答案就在于迭代器——Python中实现通用遍历的核心机制。

迭代器协议:__iter____next__

任何实现了__iter__()__next__()方法的对象,都被称为迭代器。__iter__()返回迭代器对象本身,__next__()返回容器的下一个元素,并在元素耗尽时抛出StopIteration异常。

class CountDown:
    """一个简单的迭代器,从n倒数到0"""
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        val = self.current
        self.current -= 1
        return val

# 使用示例
for num in CountDown(3):
    print(num)  # 输出 3 2 1 0

for循环的本质就是重复调用__next__()并捕获StopIteration。理解这一点,你就掌握了Python遍历的底层逻辑。

可迭代对象 vs 迭代器

一个常见的误解是混淆“可迭代对象”和“迭代器”。可迭代对象拥有__iter__()方法,通常返回一个新的迭代器;而迭代器实现了__iter__()__next__(),且__iter__()返回自身。列表、字符串是可迭代对象,但不是迭代器。你可以用iter()函数从可迭代对象获取迭代器。

nums = [1, 2, 3]
it = iter(nums)
print(next(it))  # 1
print(next(it))  # 2

这种设计允许对一个可迭代对象进行多次独立遍历。

生成器:最优雅的迭代器创建方式

编写一个类来实现迭代器有时显得繁琐。生成器提供了一种更简洁、更直观的方式——你只需像写普通函数一样,但用yield代替return

生成器函数:用yield逐值产出

任何包含yield关键字的函数都是生成器函数。调用它不会执行函数体,而是返回一个生成器对象(也是一种迭代器)。每次调用next()时,函数会执行到下一个yield语句,返回该值,并暂停状态。

def fibonacci(n):
    """生成前n个斐波那契数"""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# 使用
for num in fibonacci(10):
    print(num)

生成器自动实现了迭代器协议,并且能维持局部变量的状态——yield让函数变成了一个状态机器

生成器表达式:紧凑的懒求值

与列表推导式类似,但用圆括号包裹,就得到了生成器表达式。它不会一次性生成所有元素,而是按需产出。

# 列表推导式:立即计算,占用内存
squares_list = [x**2 for x in range(10**6)]

# 生成器表达式:惰性计算,内存友好
squares_gen = (x**2 for x in range(10**6))

生成器表达式尤其适合作为函数参数,当实参为可迭代对象时,无需额外括号:

total = sum(x**2 for x in range(10**6))

惰性求值:只在必要时才计算

生成器最强大的特性就是惰性求值(Lazy Evaluation)。值在需要时才被计算出来,这为处理大规模数据或无限序列打开了大门。

无限序列与按需生产

你可以定义一个代表所有自然数的生成器,程序不会崩溃,因为计算只在消费时发生。

def natural_numbers():
    n = 0
    while True:
        yield n
        n += 1

# 谨慎使用:需要设置跳出条件
from itertools import islice
for num in islice(natural_numbers(), 10):
    print(num)  # 只打印前10个

内存效率:处理超大文件

读取一个几十GB的日志文件,如果使用readlines()会耗尽内存。而用生成器逐行读取,内存占用始终处于常量级别。

def read_large_file(file_path):
    with open(file_path, 'r') as f:
        for line in f:
            yield line.strip()

# 处理每一行,不会把整个文件加载到内存
for log_line in read_large_file('huge.log'):
    process(log_line)

open()返回的文件对象本身就是逐行迭代的生成器,这也是Python文件处理默认高效的原因。

流式处理管道:组合生成器构建数据流水线

生成器可以像管道一样连接起来,数据从一个生成器流向另一个,无需一次性存储中间结果。这种流式处理模式让代码既清晰又高效。

示例:解析日志并提取错误信息

假设你需要从服务器日志中找出所有状态码为500的行,并提取IP地址。可以这样设计:

def parse_line(lines):
    """解析每行日志,返回(ip, status)元组"""
    for line in lines:
        parts = line.split()
        if len(parts) >= 9:
            ip = parts[0]
            # 假设状态码在第9个字段
            status = parts[8]
            yield ip, status

def filter_errors(parsed):
    """过滤出状态码为500的记录"""
    for ip, status in parsed:
        if status == '500':
            yield ip

def count_frequency(ips):
    """统计IP出现频率"""
    freq = {}
    for ip in ips:
        freq[ip] = freq.get(ip, 0) + 1
        # 此处也可以按需产出,但为了方便统计,等待所有数据流入
    # 改为流式输出也可以
    yield from freq.items()

# 构建管道
lines = read_large_file('access.log')
parsed = parse_line(lines)
errors = filter_errors(parsed)
result = count_frequency(errors)

for ip, count in result:
    print(f"{ip}: {count}次")

每个生成器函数只关心自己的任务,数据通过yield在管道中传递。你可以用yield from轻松代理子生成器的产出。

标准库中的生成器利器

itertools模块提供了大量可用于组合和操作生成器的工具,进一步加强了流式处理能力:

  • itertools.chain(*iterables):串联多个可迭代对象
  • itertools.islice(iterable, start, stop):惰性切片
  • itertools.dropwhile(predicate, iterable) / takewhile:条件过滤
  • itertools.groupby(iterable, key):分组迭代

结合生成器表达式,这些工具能让你用声明式风格写出高效的流式程序。

生成器的高级用法:send()throw()close()

除了生产值,生成器还可以通过send()方法接收值,实现协程式交互。不过对于大多数惰性计算和流式处理场景,基础的yieldyield from已完全足够。当你需要双向通信时再深入探索不迟。

何时使用生成器?

  • 数据集太大,不能全部放进内存
  • 你正在处理无限序列
  • 你希望代码更清晰地表达“按需计算”逻辑
  • 你想搭建可组合的数据处理管道

简而言之,任何时候当你写for循环时,都可以思考能否用生成器让代码更高效、更优雅。

结语

迭代器是Python遍历生态的基石,而生成器则是实现迭代器最便捷、最强大的手段。掌握生成器与惰性求值,你将拥有处理大规模数据和构建流式管道的核心能力,写出更简洁且性能优越的Python代码。