【python零基础教程第11讲】Python 迭代器与生成器
Python 迭代器与生成器:从协议到高级应用的全面指南
引言
在 Python 的编程哲学中,迭代 是一个核心概念。无论是遍历列表、读取文件,还是处理流式数据,迭代器与生成器都扮演着不可或缺的角色。它们不仅让代码更简洁、更 Pythonic,还通过 惰性求值 机制大幅优化了内存使用。然而,许多开发者对迭代器协议、yield 的底层原理、生成器与列表推导式的内存差异,以及 itertools 的高级用法仍存在模糊认识。
本文将从 迭代器协议 的完整规则出发,逐步深入到 生成器函数、生成器表达式、内存对比、生成器高级操作(send、close、throw),最后探索 itertools 工具库 的无限迭代、分组与笛卡尔积等实用技巧。通过大量代码示例与原理剖析,帮助你彻底掌握这一 Python 进阶必备技能。
一、迭代器协议:Python 迭代的基石
1.1 什么是迭代器协议?
在 Python 中,迭代器协议 是一套约定,任何对象只要实现了以下两个方法,就可以被 for 循环迭代,或用于 iter()、next() 等内置函数:
__iter__():返回迭代器对象自身(或返回一个新的迭代器)。对于容器类(如列表、元组),__iter__返回一个迭代器实例。__next__():返回容器中的下一个元素。如果没有更多元素,则抛出StopIteration异常。
注意:可迭代对象(Iterable)与迭代器(Iterator)的区别:
- 可迭代对象:实现了
__iter__()方法,返回一个迭代器。例如列表、字符串、字典。 - 迭代器:同时实现了
__iter__()和__next__()方法。迭代器可以记住遍历的位置,且只能向前不能后退。
1.2 手动实现一个迭代器
让我们通过一个简单的例子来理解协议:
class CountDown:
"""从 n 倒数到 0 的迭代器"""
def __init__(self, n):
self.n = n
def __iter__(self):
# 返回迭代器自身(因为 CountDown 实例本身就是迭代器)
return self
def __next__(self):
if self.n < 0:
raise StopIteration
current = self.n
self.n -= 1
return current
# 使用
for num in CountDown(5):
print(num) # 输出 5 4 3 2 1 0
关键点:
__iter__返回self,意味着该对象既是可迭代对象也是迭代器。__next__每次调用返回当前值并更新状态,当无元素时抛出StopIteration。for循环内部会自动捕获StopIteration并终止循环。
1.3 内置函数 iter() 与 next()
iter(obj):调用obj.__iter__()获取迭代器。next(iterator):调用iterator.__next__()获取下一个元素,可指定默认值避免异常。
lst = [1, 2, 3]
it = iter(lst)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it, 'END')) # 'END'(不会抛出 StopIteration)
1.4 迭代器协议完整规则总结
| 方法 | 作用 | 返回值 | 异常 |
|---|---|---|---|
__iter__(self) |
返回迭代器对象 | 迭代器对象(通常为 self) |
无 |
__next__(self) |
返回下一个元素 | 下一个元素 | StopIteration(无更多元素) |
重要规则:
- 迭代器对象必须实现
__iter__和__next__。 - 可迭代对象只需实现
__iter__,返回一个迭代器。 - 迭代器是 一次性 的:遍历完所有元素后,再次调用
next()会持续抛出StopIteration。 - 迭代器可以包含无限元素(如
itertools.count),此时for循环需手动 break。
二、生成器:优雅的迭代器工厂
2.1 生成器函数与 yield
生成器函数是包含 yield 关键字的函数。当调用生成器函数时,它不会立即执行函数体,而是返回一个 生成器对象(属于迭代器的一种)。每次调用 next() 时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 或函数结束。
def fibonacci(limit):
a, b = 0, 1
while a < limit:
yield a
a, b = b, a + b
gen = fibonacci(10)
for num in gen:
print(num) # 0 1 1 2 3 5 8
yield 与 return 的区别:
return终止函数并返回一个值,函数状态被销毁。yield暂停函数,保存当前所有局部变量状态,返回一个值,下次调用时恢复。
2.2 惰性取值(Lazy Evaluation)
生成器的核心优势是 惰性求值:只在需要时才计算下一个值,而不是一次性生成所有结果。这对于处理大规模数据或无限序列至关重要。
# 对比:列表推导式一次性生成所有平方数
squares_list = [x**2 for x in range(1000000)] # 占用大量内存
# 生成器表达式:惰性生成
squares_gen = (x**2 for x in range(1000000)) # 几乎不占内存
print(next(squares_gen)) # 0
print(next(squares_gen)) # 1
应用场景:
- 读取大文件(逐行读取,避免内存爆炸)
- 流式数据处理(如网络数据包、传感器数据)
- 无限序列(如自然数、斐波那契数列)
2.3 生成器表达式 vs 列表推导式:内存差异深度对比
| 特性 | 列表推导式 [expr for item in iterable] |
生成器表达式 (expr for item in iterable) |
|---|---|---|
| 返回类型 | 列表(list) | 生成器对象(generator) |
| 内存占用 | 一次性生成所有元素,占用 O(n) 内存 | 惰性求值,只保存当前状态,占用 O(1) 内存 |
| 执行时机 | 定义时立即计算 | 定义时不计算,迭代时逐个计算 |
| 可迭代次数 | 可多次迭代 | 只能迭代一次(迭代器特性) |
| 速度 | 生成列表稍快(但内存开销大) | 迭代时每次计算,可能稍慢,但内存友好 |
| 适用场景 | 数据量小、需要多次访问、需要索引 | 数据量大、只需遍历一次、无限序列 |
内存实测示例:
import sys
# 列表推导式
list_comp = [i for i in range(1000000)]
print(sys.getsizeof(list_comp)) # 约 8,000,056 字节(8MB)
# 生成器表达式
gen_expr = (i for i in range(1000000))
print(sys.getsizeof(gen_expr)) # 约 112 字节(几乎不变)
注意:生成器表达式虽然内存小,但每次迭代都有函数调用开销。如果数据量小且需要多次随机访问,列表推导式更合适。
三、生成器高级操作:send()、close()、throw()
生成器不仅仅是单向的迭代器,它还支持与调用者 双向通信。通过 send()、close()、throw() 方法,我们可以向生成器内部发送值、终止生成器或注入异常。
3.1 send(value):向生成器发送值
send() 是 next() 的增强版:它除了让生成器继续执行外,还能将 value 作为 yield 表达式的返回值传递给生成器内部。注意:第一次调用生成器时,必须使用 next(gen) 或 gen.send(None) 来启动,因为生成器尚未到达第一个 yield。
def echo():
print("开始")
while True:
received = yield # yield 表达式可以接收值
print(f"收到: {received}")
gen = echo()
next(gen) # 启动生成器,输出 "开始"
gen.send("Hello") # 输出 "收到: Hello"
gen.send("World") # 输出 "收到: World"
gen.close() # 关闭生成器
典型应用:协程(Coroutine)的雏形,用于实现简单的生产者-消费者模式。
3.2 close():关闭生成器
调用 gen.close() 会在生成器当前暂停的 yield 处抛出 GeneratorExit 异常。如果生成器内部捕获该异常并执行清理操作,则允许;否则生成器正常终止。之后再次调用 next() 或 send() 会抛出 StopIteration。
def resource_handler():
try:
print("资源打开")
yield
except GeneratorExit:
print("资源清理")
raise # 必须重新抛出,否则会报 RuntimeError
gen = resource_handler()
next(gen) # 输出 "资源打开"
gen.close() # 输出 "资源清理"
3.3 throw(type, value=None, traceback=None):向生成器注入异常
throw() 在生成器当前暂停处抛出指定异常。如果生成器内部捕获并处理了该异常,则继续执行;否则异常会传播给调用者。
def safe_divide():
try:
x = yield
y = yield
result = x / y
yield result
except ZeroDivisionError:
yield "除数不能为零"
gen = safe_divide()
next(gen) # 启动
gen.send(10) # 发送 x=10
gen.throw(ZeroDivisionError) # 注入异常,输出 "除数不能为零"
注意:throw() 返回生成器下一次 yield 的值(如果异常被捕获并继续执行),否则异常会向上传播。
四、itertools 工具库:迭代的瑞士军刀
itertools 是 Python 标准库中专门用于操作迭代器的模块,提供了大量高效、内存友好的工具函数。以下重点介绍 无限迭代、分组 和 笛卡尔积。
4.1 无限迭代器
count(start=0, step=1):生成从start开始,步长为step的无限整数序列。cycle(iterable):无限循环迭代iterable中的元素。repeat(elem, times=None):无限重复elem,若指定times则重复有限次。
from itertools import count, cycle, repeat
# 无限自然数
for i in count(1):
if i > 5: break
print(i) # 1 2 3 4 5
# 循环颜色
colors = cycle(['red', 'green', 'blue'])
for _ in range(6):
print(next(colors)) # red green blue red green blue
# 重复值
for val in repeat('A', 3):
print(val) # A A A
应用:生成唯一 ID、轮询任务、测试数据填充。
4.2 分组:groupby()
groupby(iterable, key=None) 将连续的元素分组,返回一个生成器,每个元素为 (key, group_iterator)。注意:groupby 只对 已排序 的序列有效,因为它只比较相邻元素。
from itertools import groupby
data = [('apple', 3), ('banana', 2), ('apple', 5), ('banana', 1)]
# 先按水果名排序
sorted_data = sorted(data, key=lambda x: x)
for fruit, group in groupby(sorted_data, key=lambda x: x):
print(fruit, list(group))
# 输出:
# apple [('apple', 3), ('apple', 5)]
# banana [('banana', 2), ('banana', 1)]
高级用法:结合 itemgetter 或自定义 key 函数,可用于日志分析、数据聚合等。
4.3 笛卡尔积:product()
product(*iterables, repeat=1) 计算多个可迭代对象的笛卡尔积,相当于嵌套的 for 循环。repeat 参数允许重复使用同一迭代器。
from itertools import product
# 两个列表的笛卡尔积
for p in product([1, 2], ['a', 'b']):
print(p) # (1, 'a') (1, 'b') (2, 'a') (2, 'b')
# 重复使用同一个迭代器(相当于自身笛卡尔积)
for p in product('AB', repeat=2):
print(p) # ('A','A') ('A','B') ('B','A') ('B','B')
应用:生成所有可能的参数组合(测试用例)、密码暴力破解、排列组合。
4.4 其他常用 itertools 函数速览
| 函数 | 作用 | 示例 |
|---|---|---|
chain(*iterables) |
将多个迭代器串联成一个 | chain([1,2], [3,4]) → 1,2,3,4 |
compress(data, selectors) |
根据选择器过滤数据 | compress('ABCD', [1,0,1,0]) → A,C |
dropwhile(pred, seq) |
跳过满足条件的开头元素 | dropwhile(lambda x: x<5, [1,4,6,3]) → 6,3 |
takewhile(pred, seq) |
取满足条件的开头元素 | takewhile(lambda x: x<5, [1,4,6,3]) → 1,4 |
islice(seq, start, stop, step) |
对迭代器切片 | islice(range(10), 2, 8, 2) → 2,4,6 |
zip_longest(*iterables, fillvalue) |
类似 zip,但以最长迭代器为准 | zip_longest('AB', '123', fillvalue='?') → (A,1),(B,2),(?,3) |
五、实战案例:结合生成器与 itertools 处理大文件
假设我们有一个巨大的日志文件 access.log,每行格式为 IP 时间 请求,需要统计每个 IP 的请求次数,并输出前 10 个最活跃的 IP。使用生成器逐行读取,避免内存爆炸。
from itertools import groupby, islice
from operator import itemgetter
def read_lines(file_path):
"""生成器:逐行读取文件"""
with open(file_path, 'r') as f:
for line in f:
yield line.strip()
def parse_ip(line):
"""提取 IP 地址(假设第一列)"""
return line.split()
# 1. 读取所有行(惰性)
lines = read_lines('access.log')
# 2. 提取 IP 并排序(groupby 需要排序)
ips = (parse_ip(line) for line in lines)
sorted_ips = sorted(ips) # 注意:sorted 会一次性加载所有 IP,但这里 IP 数量远小于文件行数?实际上仍可能大,可优化
# 3. 分组统计
ip_counts = ((ip, len(list(group))) for ip, group in groupby(sorted_ips))
# 4. 按次数降序排序,取前10
top10 = sorted(ip_counts, key=itemgetter(1), reverse=True)[:10]
for ip, count in top10:
print(f"{ip}: {count}")
优化:如果 IP 数量也很大,可以使用 collections.Counter 结合生成器,但 Counter 内部也是字典,内存占用可控。更极致的方案是使用 heapq.nlargest 配合生成器。
六、总结与最佳实践
6.1 何时使用迭代器/生成器?
- 数据量巨大:无法一次性加载到内存时,使用生成器惰性处理。
- 无限序列:如自然数、传感器数据流。
- 管道式处理:将多个生成器串联,形成数据处理流水线。
- 协程与异步:生成器是协程的基础(Python 3.5 后
async/await更主流,但生成器仍用于简单场景)。
6.2 性能注意事项
- 生成器每次迭代都有函数调用开销,对于小数据集,列表推导式可能更快。
- 避免在生成器内部进行复杂计算,否则会拖慢迭代速度。
- 使用
itertools函数通常比手动实现更高效(C 语言实现)。
6.3 常见陷阱
- 生成器只能迭代一次:如果需要多次遍历,请转换为列表或重新创建生成器。
groupby需要排序:否则结果不符合预期。send()第一次必须传None:否则会抛出TypeError。- 生成器中的
return语句:在 Python 3.3+ 中,return value会将value作为StopIteration的参数,可通过gen.value获取(但很少用)。
结语:迭代器与生成器是 Python 中优雅与高效的完美结合。掌握它们,你不仅能写出更节省内存的代码,还能以函数式编程的思维构建复杂的数据处理管道。从今天开始,尝试用生成器替换那些不必要的列表推导式,你会发现 Python 的世界更加广阔。
更多推荐
所有评论(0)