Python 生成器与迭代器:惰性求值与流式处理
理解迭代:从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()方法接收值,实现协程式交互。不过对于大多数惰性计算和流式处理场景,基础的yield和yield from已完全足够。当你需要双向通信时再深入探索不迟。
何时使用生成器?
- 数据集太大,不能全部放进内存
- 你正在处理无限序列
- 你希望代码更清晰地表达“按需计算”逻辑
- 你想搭建可组合的数据处理管道
简而言之,任何时候当你写for循环时,都可以思考能否用生成器让代码更高效、更优雅。
结语
迭代器是Python遍历生态的基石,而生成器则是实现迭代器最便捷、最强大的手段。掌握生成器与惰性求值,你将拥有处理大规模数据和构建流式管道的核心能力,写出更简洁且性能优越的Python代码。