在Python开发中,for循环能遍历列表、字典甚至文件,背后依赖的是迭代器协议。而yield关键字塑造的生成器,让迭代器定义变得简洁,并天然支持惰性求值。掌握这两者,能写出更Pythonic的代码,处理大数据流时显著降低内存占用,甚至为理解协程和异步编程打下基础。本文从协议原理出发,通过可运行的实战代码,彻底吃透生成器与迭代器。
一、迭代器协议
Python中,所有可用for ... in ...的对象都是可迭代对象(Iterable),其内部实现__iter__方法返回一个迭代器(Iterator)。迭代器必须实现__iter__(通常返回自身)和__next__方法,每次调用__next__返回下一个元素,无元素时抛出StopIteration异常。for循环本质就是:调用可迭代对象的__iter__获得迭代器,反复调用__next__获取值,捕获StopIteration退出。
下面是一个手动实现的反向计数器迭代器类:- class Countdown:
- def __init__(self, start):
- self.current = start
- def __iter__(self):
- return self
- def __next__(self):
- if self.current < 0:
- raise StopIteration
- value = self.current
- self.current -= 1
- return value
- cd = Countdown(3)
- for num in cd:
- print(num) # 输出 3,2,1,0
复制代码
内置函数iter()和next()直接调用这些特殊方法。如果对象不是迭代器,iter()会调用__iter__;同一个对象可多次调用iter()获得独立的迭代器。
二、生成器函数:迭代器工厂
每次定义迭代器都要手动维护状态和异常,比较繁琐。生成器函数(generator function)提供了更优雅的方案:函数体内包含yield关键字,Python就会将该函数编译为生成器。调用生成器函数不执行函数体,而是返回一个生成器对象,自动实现迭代器协议。- def countdown_gen(start):
- while start >= 0:
- yield start
- start -= 1
- cd_gen = countdown_gen(3)
- print(type(cd_gen)) # <class 'generator'>
- for num in cd_gen:
- print(num) # 输出 3,2,1,0
复制代码
每次调用next()或迭代执行到yield时,函数暂停并返回值,保留局部状态;下一次从暂停处恢复运行,直到函数结束自动抛出StopIteration。这种协程机制使生成器既像函数又像轻量级线程,为异步编程提供基础。
三、生成器表达式:懒加载的列表推导
生成器表达式使用圆括号而非方括号,返回生成器对象,元素按需生成,不立即计算全部值,内存占用极小。- squares_list = [x*x for x in range(10)] # 立即生成全部
- squares_gen = (x*x for x in range(10)) # 懒加载
- print(next(squares_gen)) # 0
- print(list(squares_gen)) # [1,4,9,...,81](注意第一个0已消耗)
复制代码
生成器表达式作为函数参数时可省略一组括号,例如sum(x*x for x in range(10))。
四、实战示例
示例1:读取超大日志文件并实时处理
假设有数百MB日志文件,逐行解析并统计错误。一次性读取会撑爆内存,使用生成器可以流式处理。- def read_large_file(file_path):
- with open(file_path, 'r', encoding='utf-8') as f:
- for line in f:
- yield line.strip()
- def count_errors(log_path):
- error_count = 0
- for line in read_large_file(log_path):
- if "ERROR" in line:
- error_count += 1
- print(f"发现错误: {line[:50]}...")
- return error_count
- # error_total = count_errors("server.log")
- # print(f"共 {error_total} 条错误")
复制代码
read_large_file是生成器,每次只读取一行,内存仅保留当前行。
示例2:斐波那契数列的无穷生成器- def fibonacci():
- a, b = 0, 1
- while True:
- yield a
- a, b = b, a + b
- fib = fibonacci()
- for _ in range(10):
- print(next(fib), end=' ') # 0 1 1 2 3 5 8 13 21 34
复制代码
调用方按需获取数据,典型惰性求值。
示例3:使用yield from委派子生成器- def chain_generators(*iterables):
- for it in iterables:
- yield from it
- combined = chain_generators([1,2,3], (4,5), "AB")
- print(list(combined)) # [1, 2, 3, 4, 5, 'A', 'B']
复制代码
yield from 简化嵌套循环,并支持双向通信。
示例4:生成器的send与close- def accumulator():
- total = 0
- while True:
- value = yield total
- if value is None:
- break
- total += value
- return total
- acc = accumulator()
- next(acc) # 预激,执行到第一个yield
- print(acc.send(10)) # 10
- print(acc.send(5)) # 15
- acc.send(None) # 触发break,停止生成器
- try:
- acc.send(0)
- except StopIteration as e:
- print(f"最终返回值: {e.value}") # 15
复制代码
注意:生成器需要先调用next()或send(None)启动,否则抛出TypeError。
五、常见问题与注意事项
1. 只能遍历一次:迭代器是“一次性”的,遍历结束或close()后,再次迭代无值。需重复使用可重新调用生成器函数或转为列表。
- gen = (x for x in range(3))
- print(list(gen)) # [0,1,2]
- print(list(gen)) # [] 空列表
复制代码
2. StopIteration异常处理:for循环自动捕获;手动调用next()需处理异常。生成器函数return的值(Python 3.3+)保存在StopIteration.value中。
- def my_gen():
- yield 1
- return "Done"
- g = my_gen()
- print(next(g)) # 1
- try:
- next(g)
- except StopIteration as e:
- print(e.value) # "Done"
复制代码
3. 变量作用域与生命周期:yield暂停后局部变量依然保留。注意闭包或外部变量引用可能导致意外大对象持有,内存不释放。可用函数参数或局部变量明确传递数据。
4. yield from的子生成器关闭:外层生成器调用close()或throw()时,异常会传给子生成器。子生成器应使用try/finally确保资源释放。
5. 性能与选择:生成器节省内存,但每次yield有状态保存开销。小数据集用列表推导可能更快。优化原则:先保证代码清晰,内存成为瓶颈再考虑生成器。用timeit和内存监控工具辅助决策。
6. 生成器与协程的关系:生成器是协程的底层实现基础。Python 3.5+的async/await本质基于生成器封装。理解send、throw、close,能降低学习异步编程的难度。
六、总结
迭代器和生成器是Python迭代与流式处理的核心机制。迭代器协议统一遍历接口,生成器以简洁语法和惰性求值特性,让我们轻松处理序列化数据、构建高效管道并控制内存占用。通过本文,你应当能理解__iter__和__next__如何驱动for循环,熟练使用生成器函数和表达式,掌握yield from和send/close等操作,避免常见陷阱。在大数据流、多层嵌套遍历、数据处理管道等场景,善用生成器将使代码更优雅、更高效。 |