一. 为什么需要生成器?
在Python编程中,我们经常需要处理大量数据或无限序列。如果一次性将所有数据加载到内存中,可能导致内存溢出。生成器(Generator)提供了一种惰性求值(Lazy Evaluation)的机制:它们可以逐个产生值,而不是一次性构建整个序列。
举个例子:读取一个10GB的日志文件,如果用`readlines()`会将整个文件加载到内存,而使用生成器逐行读取则只需要少量内存。生成器也是Python实现协程(Coroutine)的基础,而协程又是现代异步编程的核心。
本文将带你从最基础的迭代器概念开始,逐步深入生成器的底层原理、协程的演变,再到实战中的高阶应用。
二 . 迭代器协议与可迭代对象
理解生成器之前,必须先搞懂迭代器(Iterator)和可迭代对象(Iterable)。
2.1 可迭代对象(Iterable)
任何实现了`iter()`方法的对象都是可迭代对象。`iter()`应该返回一个迭代器。常见的可迭代对象包括:列表、元组、字符串、字典、集合等。
python
# 判断是否为可迭代对象
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance(123, Iterable)) # False
2.2 迭代器(Iterator)
迭代器实现了两个方法:`iter()` 和 `next()`。`iter()`返回自身,`next()`返回下一个元素,当没有元素时抛出`StopIteration`异常。
python
class MyRange:
"""自定义一个可迭代对象(同时也是迭代器)"""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# 使用
for num in MyRange(1, 5):
print(num) # 1 2 3 4
# 手动迭代
it = MyRange(1, 3)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 抛出StopIteration
手动实现迭代器比较繁琐,生成器则提供了简便的创建方式。
三. 生成器函数:`yield` 的魔法
生成器函数是指包含`yield`关键字的函数。当调用生成器函数时,它不会立即执行函数体,而是返回一个生成器对象(属于迭代器的一种)。
3.1 第一个生成器
python
class MyRange:
"""自定义一个可迭代对象(同时也是迭代器)"""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# 使用
for num in MyRange(1, 5):
print(num) # 1 2 3 4
# 手动迭代
it = MyRange(1, 3)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 抛出StopIteration
每次调用`next()`,生成器函数会从上一次`yield`暂停的地方继续执行,直到遇到下一个`yield`或函数返回。
3.2 生成器的常见用法:遍历大数据
python
def read_large_file(file_path):
"""逐行读取大文件,每次只加载一行到内存"""
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()
# 使用示例(假设有一个10GB的日志文件)
for line in read_large_file('huge_log.txt'):
process_line(line) # 每次处理一行,内存占用极小
如果没有生成器,传统的`readlines()`会一次性将所有行读入列表,导致内存爆炸。
3.3 生成器的惰性特性
生成器不会计算所有值,而是按需计算。这在构造无限序列时非常有用。
python
def fibonacci():
"""无限斐波那契数列生成器"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib)) # 0,1,1,2,3,5,8,13,21,34
3.4 生成器与列表推导式的对比
列表推导式会立即生成整个列表,而生成器表达式则是惰性的。
python
import sys
# 列表推导式:占用大量内存
list_squares = [x**2 for x in range(1000000)]
print(sys.getsizeof(list_squares)) # 约 8MB+
# 生成器表达式:几乎不占内存
gen_squares = (x**2 for x in range(1000000))
print(sys.getsizeof(gen_squares)) # 约 120 字节
# 但结果相同
print(next(gen_squares)) # 0
print(next(gen_squares)) # 1
四. 生成器表达式
生成器表达式语法与列表推导式类似,但使用圆括号而不是方括号。它是创建简单生成器的便捷方式。
python
# 语法: (expression for item in iterable if condition)
even_gen = (x for x in range(10) if x % 2 == 0)
for num in even_gen:
print(num) # 0 2 4 6 8
生成器表达式可以组合使用,例如从多个源生成:
pairs = ((x, y) for x in range(3) for y in range(2))
print(list(pairs)) # [(0,0),(0,1),(1,0),(1,1),(2,0),(2,1)]
注意:生成器表达式只能使用一次,迭代完毕后不能再使用。
gen = (x for x in range(3))
print(sum(gen)) # 3
print(sum(gen)) # 0,因为已经耗尽
五. 深入生成器:`send()`、`throw()` 与 `close()`
生成器不仅仅是`yield`值的单向管道。通过额外的方法,我们可以与生成器进行双向通信。
5.1 `send(value)` 方法
`send()`可以将一个值发送给生成器,成为`yield`表达式的返回值。首次调用必须传入`None`或者直接调用`next()`来启动生成器。
python
def echo_generator():
received = yield "准备就绪"
print(f"收到: {received}")
received = yield f"已收到: {received}"
print(f"再次收到: {received}")
yield "结束"
gen = echo_generator()
print(next(gen)) # 准备就绪(启动生成器)
print(gen.send("Hello")) # 发送Hello,输出"收到: Hello",然后返回"已收到: Hello"
print(gen.send("World")) # 发送World,输出"再次收到: World",然后返回"结束"
# next(gen) # StopIteration
更实用的例子:使用生成器实现可交互的计数器。
python
def accumulative_counter():
"""累加计数器,发送新的数值进行累加"""
total = 0
while True:
increment = yield total
if increment is None:
break
total += increment
counter = accumulative_counter()
next(counter) # 启动,返回初始total=0
print(counter.send(5)) # 5
print(counter.send(3)) # 8
print(counter.send(2)) # 10
5.2 `throw(type, value=None, traceback=None)` 方法
在生成器当前暂停的`yield`处抛出一个异常。如果生成器内部捕获了该异常并继续执行,则返回下一个yield的值;否则异常会传播。
python
def catch_exception_gen():
try:
yield "正常运行"
except ValueError:
print("捕获到ValueError")
yield "处理异常后继续"
yield "结束"
gen = catch_exception_gen()
print(next(gen)) # 正常运行
print(gen.throw(ValueError)) # 捕获到ValueError \n 处理异常后继续
print(next(gen)) # 结束
5.3 `close()` 方法
停止生成器。在生成器内部,如果在`yield`处被关闭,会抛出`GeneratorExit`异常(不会被捕获,除非主动捕获并最终`return`)。
python
def generator_with_cleanup():
try:
yield 1
yield 2
except GeneratorExit:
print("执行清理工作")
return
gen = generator_with_cleanup()
print(next(gen)) # 1
gen.close() # 执行清理工作
# 再调用next(gen) 会抛出StopIteration
六. 协程基础:从生成器到协程的演变
生成器可以暂停和恢复执行,而协程(Coroutine)更进一步:它可以让生成器不仅向外产出值,还能从外部接收值。因此,生成器可以当作协程使用。在Python 3.5之前,协程就是基于生成器实现的。
6.1 简单协程实现任务切换
下面展示一个使用生成器实现的多任务协作调度示例:
python
def task1():
for i in range(3):
print(f"任务1 执行第{i}步")
yield
def task2():
for i in range(3):
print(f"任务2 执行第{i}步")
yield
def scheduler(*tasks):
"""简单任务调度器,轮流执行每个任务一步"""
tasks = list(tasks) # 每个任务都是一个生成器
while tasks:
task = tasks.pop(0)
try:
next(task)
tasks.append(task) # 未完成的任务放回队尾
except StopIteration:
pass
scheduler(task1(), task2())
输出:
```
任务1 执行第0步
任务2 执行第0步
任务1 执行第1步
任务2 执行第1步
任务1 执行第2步
任务2 执行第2步
```
这展示了协作式多任务的基本思想,也是`asyncio`事件循环的雏形。
6.2 使用`send`实现协程之间的双向通信
python
def printer():
"""协程:打印接收到的消息"""
while True:
msg = yield
print(f"打印: {msg}")
def repeater(count):
"""协程:重复发送消息"""
for i in range(count):
msg = yield
yield f"重复: {msg}"
# 创建两个协程
p = printer()
next(p) # 启动
r = repeater(2)
next(r) # 启动
# 发送消息给repeater,再转发给printer
p.send(r.send("Hello"))
p.send(r.send("World"))
注意:`yield`既可以产出值(右侧),也可以接收值(左侧赋值)。这体现了生成器的双向能力。
七. `yield from`:委托生成器
Python 3.3引入了`yield from`语法,用于将迭代过程委托给另一个生成器(或任何可迭代对象)。它简化了生成器的嵌套和扁平化,也为协程提供了更好的支持。
7.1 基本用法:扁平化嵌套生成器
python
def sub_generator():
yield 1
yield 2
yield 3
def delegating_generator():
yield "开始"
yield from sub_generator() # 委托
yield "结束"
for value in delegating_generator():
print(value) # 开始 1 2 3 结束
不使用`yield from`的等效写法:
def old_way():
yield "开始"
for value in sub_generator():
yield value
yield "结束"
7.2 扁平化嵌套列表
`yield from`非常适合递归地处理嵌套结构:
python
def flatten(nested_list):
"""扁平化任意嵌套的列表"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item) # 递归委托
else:
yield item
nested = [1, [2, [3, 4], 5], 6]
print(list(flatten(nested))) # [1, 2, 3, 4, 5, 6]
7.3 `yield from` 在协程中的作用
当`yield from`应用于生成器对象时,它会建立双向通道,将调用方的`send()`、`throw()` 和 `close()` 直接传递给子生成器。这使得子生成器可以完全替代当前的生成器,是实现协程委托的关键。
python
def sub_coroutine():
try:
while True:
x = yield
print(f"子协程收到: {x}")
except GeneratorExit:
print("子协程清理")
def main_coroutine():
yield from sub_coroutine() # 委托
print("主协程结束")
main = main_coroutine()
next(main) # 启动,进入子协程
main.send("Hello") # 子协程收到: Hello
main.send("World") # 子协程收到: World
main.close() # 子协程清理
八. 异步编程基石:`asyncio` 与原生协程(`async/await`)
从Python 3.5开始,引入了`async`和`await`关键字,用于定义原生协程和等待异步操作。虽然它们与生成器在底层有关联,但语法和语义更清晰,且不能包含`yield`(但可以使用`await`等待另一个协程)。
8.1 原生协程的定义与调用
python
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1)
print("World")
return "完成"
# 不能直接调用协程函数,必须通过事件循环执行
# say_hello() # 返回一个协程对象,不会执行
async def main():
result = await say_hello()
print(result)
# 运行事件循环
asyncio.run(main())
8.2 并发执行多个任务
python
import asyncio
async def task(name, delay):
print(f"任务{name} 开始,延迟{delay}秒")
await asyncio.sleep(delay)
print(f"任务{name} 完成")
return f"{name}的结果"
async def main():
# 创建多个协程任务
tasks = [
asyncio.create_task(task("A", 2)),
asyncio.create_task(task("B", 1)),
asyncio.create_task(task("C", 3))
]
# 等待所有任务完成
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
输出顺序:任务B最先完成,然后A,最后C,体现了并发执行。
8.3 将阻塞代码转为异步
使用`loop.run_in_executor`将同步阻塞函数放到线程池中执行,避免阻塞事件循环。
python
import asyncio
import time
def blocking_io():
time.sleep(2) # 模拟阻塞操作
return "阻塞结果"
async def async_operation():
loop = asyncio.get_running_loop()
# 在线程池中执行阻塞函数
result = await loop.run_in_executor(None, blocking_io)
print(result)
asyncio.run(async_operation())
8.4 原生协程与生成器协程的关系
实际上,原生协程底层仍然使用了生成器机制,但更严格:原生协程不能使用`yield`,只能使用`await`。我们可以通过`inspect`模块检测:
python
import asyncio
import inspect
async def native_coro():
await asyncio.sleep(0)
def generator_coro():
yield
print(inspect.isgeneratorfunction(generator_coro)) # True
print(inspect.iscoroutinefunction(native_coro)) # True
九. 实战应用场景
9.1 大数据流式处理
假设有一个10GB的CSV文件,我们想按需处理每一行,并过滤、转换,最后写入另一个文件。生成器链式处理非常优雅:
python
import csv
def read_csv(file_path):
with open(file_path, 'r') as f:
reader = csv.reader(f)
for row in reader:
yield row
def filter_rows(rows, predicate):
for row in rows:
if predicate(row):
yield row
def transform_rows(rows, transformer):
for row in rows:
yield transformer(row)
def write_csv(rows, output_path):
with open(output_path, 'w', newline='') as f:
writer = csv.writer(f)
for row in rows:
writer.writerow(row)
# 使用流水线
rows = read_csv('large_input.csv')
filtered = filter_rows(rows, lambda r: int(r[2]) > 1000)
transformed = transform_rows(filtered, lambda r: [r[0], r[1], str(int(r[2]) * 2)])
write_csv(transformed, 'output.csv')
整个过程是惰性的,内存中始终只保存一行数据。
9.2 无限序列生成
在数学计算或游戏开发中,经常需要无穷序列:
python
def primes():
"""无限素数生成器(埃拉托色尼筛法)"""
yield 2
yield 3
primes_so_far = [2, 3]
num = 5
while True:
is_prime = True
for p in primes_so_far:
if p * p > num:
break
if num % p == 0:
is_prime = False
break
if is_prime:
primes_so_far.append(num)
yield num
num += 2
# 取前20个素数
from itertools import islice
first_20 = list(islice(primes(), 20))
print(first_20)
9.3 协程实现状态机
状态机在游戏AI、网络协议解析中很常见。用协程实现非常直观:
python
def state_machine():
"""简单的门状态机:closed -> opened -> closed"""
state = "closed"
while True:
if state == "closed":
event = yield "门已关闭"
if event == "open":
state = "opened"
elif state == "opened":
event = yield "门已打开"
if event == "close":
state = "closed"
sm = state_machine()
next(sm) # 启动
print(sm.send("open")) # 门已打开
print(sm.send("close")) # 门已关闭
9.4 简化异步回调(将回调包装为协程)
在异步编程中,有些老的库仍然使用回调函数。可以使用`asyncio.Future`将其包装成协程:
python
import asyncio
def old_callback_based_api(callback):
"""模拟旧式回调API,1秒后调用callback"""
import threading
def worker():
import time
time.sleep(1)
callback("数据结果")
threading.Thread(target=worker).start()
async def wrapper_api():
loop = asyncio.get_running_loop()
future = loop.create_future()
def set_future(result):
future.set_result(result)
old_callback_based_api(set_future)
result = await future
return result
async def main():
data = await wrapper_api()
print(data) # 数据结果
asyncio.run(main())
十. 总结
本文详细讲解了Python生成器与协程的完整知识体系:
-
生成器基础:包含`yield`的函数,返回生成器对象,支持惰性求值。
-
迭代器协议:生成器是迭代器的子集。
-
生成器表达式:简洁的惰性序列创建方式。
-
双向通信:`send()`, `throw()`, `close()`方法让生成器可以接收外部数据。
-
`yield from`:委托生成器,扁平化嵌套,简化协程代理。
-
原生协程:`async/await`语法,是当前异步编程的标准。
-
实战应用:流式处理、无限序列、状态机、轻量级调度器等。
-
性能对比:内存节省巨大,速度略微下降,适合I/O密集与大数据场景。
生成器和协程是Python高级编程的利器。掌握它们,你就能写出更高效、更优雅的代码。无论是在数据处理、网络爬虫、Web后端还是游戏开发中,生成器和协程都会让你事半功倍。
希望这篇详细的教程能够帮助你彻底理解Python生成器与协程,并在实际项目中灵活运用。