Python生成器是语言中最优雅且强大的特性之一。它不仅提供了一种惰性求值的数据生产方式,更是Python协程机制的基石。从简单的迭代器替代到复杂的异步IO框架,生成器的身影无处不在。然而,许多开发者对生成器的理解停留在
1 | yield |
关键字的表面用法,对其底层机制、
1 | send() |
与
1 | throw() |
的双向通信、
1 | yield from |
的子生成器委托,以及从生成器到原生协程的演进历程缺乏系统认识。本文将从迭代器协议出发,逐层深入,带你全面掌握Python生成器的核心原理与实战技巧。
一、迭代器协议:生成器的根基
理解生成器的第一步,是理解Python的迭代器协议。在Python中,任何实现了
1 | __iter__() |
和
1 | __next__() |
方法的对象都是迭代器。
1 | __iter__ |
返回迭代器对象自身,
1 | __next__ |
返回下一个值,当没有更多元素时抛出
1 | StopIteration |
异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# 使用方式
for num in Countdown(5):
print(num) # 5, 4, 3, 2, 1
手动实现迭代器需要编写大量样板代码,尤其是
1 | __iter__ |
和
1 | __next__ |
的声明、状态管理和
1 | StopIteration |
的抛出。生成器正是为了简化这一过程而诞生的——它让Python解释器自动为你生成迭代器。
二、生成器函数与yield表达式
包含
1 | yield |
关键字的函数不再是普通函数,而是一个生成器工厂。调用它不会执行函数体,而是返回一个生成器对象。生成器对象自动实现了迭代器协议,每次调用
1 | next() |
时,函数体从上一次
1 | yield |
的位置恢复执行,直到遇到下一个
1 | yield |
或函数结束。
1
2
3
4
5
6
7
8
9
10
11 def countdown(start):
current = start
while current > 0:
yield current
current -= 1
gen = countdown(5)
print(type(gen)) # <class 'generator'>
print(next(gen)) # 5
print(next(gen)) # 4
print(list(gen)) # [3, 2, 1]
2.1 生成器的内部状态保存
生成器的核心魔法在于挂起与恢复。当执行到
1 | yield |
时,生成器会保存当前的执行帧(包括局部变量、指令指针和异常状态),将控制权交还给调用者。下次
1 | next() |
时,从保存的状态精确恢复。这一机制使得生成器天然适合处理流式数据和无限序列。
1
2
3
4
5
6
7
8
9
10 def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 惰性求值:只计算需要的值
fib = fibonacci()
first_ten = [next(fib) for _ in range(10)]
print(first_ten) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
2.2 生成器表达式
与列表推导式类似,生成器表达式提供了一种简洁的生成器创建方式,使用圆括号而非方括号。它的惰性求值特性使得内存占用从O(n)降为O(1)。
1
2
3
4
5
6
7
8
9
10
11
12
13 import sys
# 列表推导式:一次性生成所有元素
nums_list = [x ** 2 for x in range(1_000_000)]
print(sys.getsizeof(nums_list)) # ~8.5MB
# 生成器表达式:惰性生成
nums_gen = (x ** 2 for x in range(1_000_000))
print(sys.getsizeof(nums_gen)) # ~200 bytes
# 生成器表达式可直接传入sum/max/min等函数
total = sum(x ** 2 for x in range(100))
print(total) # 328350
三、双向通信:send()、throw()与close()
生成器不仅是数据的生产者,还可以是数据的消费者。
1 | yield |
实际上是一个表达式,它可以接收外部通过
1 | send() |
传入的值。这使得生成器具备了协程的雏形。
3.1 send()方法与值传递
1 | send(value) |
将
1 | value |
发送到生成器内部,作为当前
1 | yield |
表达式的返回值,然后生成器恢复执行到下一个
1 | yield |
。注意:第一次调用
1 | send() |
必须传入
1 | None |
(或者先用
1 | next() |
启动),因为生成器还没有执行到
1 | yield |
表达式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 def accumulator():
total = 0
while True:
value = yield total # yield产出total,同时接收send传入的value
if value is None:
break
total += value
gen = accumulator()
next(gen) # 启动生成器,执行到第一个yield,产出0
print(gen.send(10)) # 发送10,yield表达式返回10,total=10,产出10
print(gen.send(20)) # 发送20,yield表达式返回20,total=30,产出30
print(gen.send(5)) # 发送5,total=35,产出35
gen.close() # 关闭生成器
3.2 throw()方法与异常注入
1 | throw(typ[, val[, tb]]) |
在生成器挂起的
1 | yield |
位置抛出指定异常。这为生成器提供了从外部中断或信号通知的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 def resilient_processor():
while True:
try:
data = yield
print(f"Processing: {data}")
except ValueError as e:
print(f"Skipping bad data: {e}")
except GeneratorExit:
print("Cleaning up resources...")
raise
gen = resilient_processor()
next(gen) # 启动
gen.send("normal data") # Processing: normal data
gen.throw(ValueError, "bad format") # Skipping bad data: bad format
gen.send("resumed") # Processing: resumed
gen.close() # Cleaning up resources...
3.3 close()方法与清理
1 | close() |
在生成器内部抛出
1 | GeneratorExit |
异常。如果生成器捕获了该异常,应执行清理逻辑后重新抛出或直接返回。忽略
1 | GeneratorExit |
会导致
1 | RuntimeError |
。
四、yield from:子生成器委托
Python 3.3引入的
1 | yield from |
是生成器演进的关键一步。它不仅简化了生成器嵌套的写法,更建立了主生成器与子生成器之间的透明双向通道——
1 | send() |
、
1 | throw() |
和
1 | close() |
调用会自动穿透到子生成器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 def sub_gen():
received = yield "Sub started"
yield f"Sub received: {received}"
return "Sub result"
def main_gen():
# yield from建立双向通道
result = yield from sub_gen()
yield f"Main got sub result: {result}"
gen = main_gen()
print(next(gen)) # Sub started
print(gen.send("from main")) # Sub received: from main
print(next(gen)) # Main got sub result: Sub result
4.1 yield from的核心语义
1 | yield from <iterable> |
的行为可以概括为:
- 迭代阶段:自动从子生成器获取值并向上产出,相当于一个
1for item in sub: yield item
的简化
- 值传递:调用者
1send()
的值直接传递给子生成器的
1yield表达式
- 异常传递:调用者
1throw()
的异常直接在子生成器的
1yield处抛出
- 返回值捕获:子生成器的
1return
值成为
1yield from表达式的值
这意味着
1 | yield from |
不仅仅是语法糖,它建立了一个语义等价的委托关系——对于调用者而言,主生成器和子生成器就像一个统一的生成器。
4.2 实战:用yield from构建数据处理管道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 def read_lines(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
stripped = yield line.strip()
def filter_comments(source):
for line in source:
if not line.startswith('#') and line:
yield line
def parse_config(source):
lines = yield from read_lines(source)
for line in filter_comments(lines):
if '=' in line:
key, value = line.split('=', 1)
yield (key.strip(), value.strip())
# 使用管道
config = dict(parse_config('/etc/app.conf'))
五、从生成器协程到async/await
Python的异步编程经历了三个重要阶段,而生成器是其中的关键纽带。
5.1 早期协程:基于生成器的协程
在Python 3.5引入
1 | async/await |
之前,开发者利用
1 | yield |
和
1 | yield from |
实现了协程。通过
1 | types.coroutine |
装饰器或
1 | asyncio.coroutine |
装饰器,生成器函数可以被标记为协程。
1
2
3
4
5
6
7
8
9
10
11
12
13 import asyncio
import types
@types.coroutine
def old_style_coroutine():
result = yield from asyncio.sleep(1)
print("Waited 1 second")
return result
# 现代写法对比
async def modern_coroutine():
await asyncio.sleep(1)
print("Waited 1 second")
5.2 原生协程与生成器的区别
Python 3.5引入的
1 | async def |
定义的是原生协程,它与生成器有以下关键区别:
| 特性 | 生成器协程 | 原生协程 |
|---|---|---|
| 定义方式 | 包含yield的函数 | async def |
| 返回类型 | generator | coroutine |
| 暂停点 | yield / yield from | await |
| 可迭代 | 是(可用next()) | 否 |
| 事件循环 | 需手动驱动 | asyncio自动调度 |
关键洞察:
1 | await |
的语义与
1 | yield from |
高度相似——都是将控制权交给被委托的对象,并等待其完成。
1 | await |
实际上是
1 | yield from |
在异步上下文中的专有形式,限制只能用于awaitable对象(实现了
1 | __await__() |
方法的对象)。
六、异步生成器:async yield
Python 3.6引入了异步生成器,将
1 | async def |
与
1 | yield |
结合,使得在异步上下文中也能进行惰性求值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import asyncio
import aiohttp
async def fetch_paginated(api_url, page_size=100):
page = 1
async with aiohttp.ClientSession() as session:
while True:
async with session.get(
f"{api_url}?page={page}&size={page_size}"
) as resp:
data = await resp.json()
if not data['items']:
break
for item in data['items']:
yield item
page += 1
async def process_all():
async for item in fetch_paginated("https://api.example.com/data"):
print(f"Processing: {item['id']}")
asyncio.run(process_all())
异步生成器的关键规则:
- 使用
1async for
迭代,不能用普通
1for - 使用
1async with
进行异步上下文管理
- 每个
1yield
前可以有
1await调用
- 异步生成器没有
1return value
(返回值会被忽略)
- 使用
1aclose()
而非
1close()关闭
七、生成器实战模式
7.1 流式处理大文件
处理GB级日志文件时,一次性加载到内存是不可行的。生成器提供了一种优雅的流式处理方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 def parse_log_stream(filepath, pattern=None):
import re
regex = re.compile(pattern) if pattern else None
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
if regex and not regex.search(line):
continue
parts = line.split(' ', 3)
if len(parts) >= 4:
yield {
'timestamp': parts[0] + ' ' + parts[1],
'level': parts[2].rstrip(':'),
'message': parts[3],
'line': line_num
}
# 链式处理
errors = (e for e in parse_log_stream('/var/log/app.log', r'ERROR'))
critical = (e for e in errors if 'CRITICAL' in e['message'])
for entry in critical:
print(f"[{entry['timestamp']}] {entry['message']}")
7.2 状态机实现
生成器的
1 | send() |
方法天然适合实现状态机——每个
1 | yield |
就是一个状态节点,
1 | send() |
传入的值是状态转移的输入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 def http_parser():
state = 'METHOD'
method = path = version = ''
headers = {}
body_chunks = []
while True:
data = yield
if data is None:
continue
if state == 'METHOD':
parts = data.split(' ')
if len(parts) >= 3:
method, path, version = parts[0], parts[1], parts[2]
state = 'HEADERS'
elif state == 'HEADERS':
if data == '':
state = 'BODY'
elif ':' in data:
key, val = data.split(':', 1)
headers[key.strip()] = val.strip()
elif state == 'BODY':
body_chunks.append(data)
parser = http_parser()
next(parser)
parser.send('GET /api/users HTTP/1.1')
parser.send('Host: example.com')
parser.send('Content-Type: application/json')
parser.send('')
parser.send('{"query": "active"}')
7.3 上下文管理器与生成器
1 | contextlib.contextmanager |
装饰器将生成器函数转换为上下文管理器,这是标准库中最优雅的生成器应用之一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from contextlib import contextmanager
import time
@contextmanager
def timer(label="Operation"):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label} took {elapsed:.4f}s")
with timer("Data loading"):
data = load_large_dataset()
with timer("Model training"):
model.fit(data)
八、生成器性能陷阱与最佳实践
8.1 常见陷阱
陷阱一:生成器只能消费一次。生成器是单次迭代器,消费完毕后再次迭代不会产出任何值。如果需要多次迭代,应使用
1 | list() |
物化或
1 | itertools.tee() |
。
1
2
3
4
5
6
7
8
9
10
11
12
13 def numbers():
yield from range(5)
gen = numbers()
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] -- already exhausted!
# Solution 1: materialize
data = list(numbers())
# Solution 2: factory function
def numbers_factory():
return (x for x in range(5))
陷阱二:生成器中的异常被吞噬。当生成器未完全消费时(如在
1 | for |
循环中
1 | break |
),生成器被垃圾回收时会在
1 | yield |
处抛出
1 | GeneratorExit |
。如果此时有未处理的异常,可能掩盖真正的错误。
陷阱三:过度嵌套yield from。深度嵌套的
1 | yield from |
链会增加调用栈深度,在极端情况下影响性能。对于简单的迭代转发,优先考虑
1 | itertools.chain |
。
8.2 最佳实践
- 明确区分生成器函数与普通函数:一旦函数包含
1yield
,它就是生成器工厂。不要在生成器函数中混用
1return value(Python 3.3+中
1return value会设置
1StopIteration.value,但容易被忽略)。
- 使用类型注解标注生成器:
1Generator[YieldType, SendType, ReturnType]
比
1Iterator[YieldType]更精确地描述了生成器的完整类型。
- 及时关闭不再使用的生成器:调用
1close()
确保资源释放,尤其是在生成器持有文件句柄或数据库连接时。
- 优先使用生成器表达式替代列表推导:当结果只需要被消费一次且不需要索引访问时,生成器表达式更节省内存。
1
2
3
4
5
6
7 from typing import Generator
def structured_generator(n: int) -> Generator[int, str, None]:
for i in range(n):
message = yield i
if message:
print(f"Received: {message}")
九、总结
Python生成器从迭代器协议出发,经历了
1 | yield |
表达式、
1 | send() |
双向通信、
1 | yield from |
子生成器委托,最终演进出原生协程
1 | async/await |
和异步生成器
1 | async yield |
。这一演进历程体现了Python语言设计的核心哲学——用统一的抽象解决不同层次的问题。
掌握生成器的关键要点:
- 生成器函数是迭代器工厂,
1yield
实现挂起与恢复
-
1send()
/
1throw()/
1close()提供双向通信与生命周期控制
-
1yield from
建立透明的子生成器委托通道
- 原生协程是生成器协程的演进,
1await
是
1yield from的异步版本
- 异步生成器将惰性求值引入异步编程
- 实战中注意生成器的单次消费、异常处理和资源释放
生成器是Python中少有的“小而美”的特性——概念简单,却能组合出极其强大的抽象。无论是流式数据处理、状态机实现,还是协程与异步IO,生成器都是Python程序员工具箱中不可或缺的利器。深入理解生成器,不仅能写出更高效、更Pythonic的代码,更能帮助你理解asyncio等现代异步框架的底层运行机制。
汤不热吧