1. 为什么我们需要生成器?
当你在Python中处理一个包含1000万个元素的列表时,内存会立即被全部占用。而生成器则像一位精明的管家,只在需要时才"生产"下一个值。这种按需计算的特性,我们称之为惰性求值(Lazy Evaluation)。
我第一次真正理解生成器的价值是在处理一个大型日志文件时。传统方法是将整个文件读入内存,结果导致16GB内存的服务器直接崩溃。改用生成器后,内存使用量始终保持在几MB的水平。
1.1 内存效率的对比实验
让我们用实际数据说话。创建一个包含1亿个数字的序列:
python复制# 列表方式
import sys
nums_list = [x for x in range(100000000)]
print(sys.getsizeof(nums_list)) # 输出:900000112字节(约858MB)
# 生成器方式
nums_gen = (x for x in range(100000000))
print(sys.getsizeof(nums_gen)) # 输出:128字节
这个对比令人震惊——生成器几乎不占用额外内存,无论序列有多长。它只是保存了生成算法和当前状态,而不是整个数据集。
1.2 计算与消费的解耦
生成器实现了生产者和消费者模式的优雅解耦。想象一个实时股票数据流:
python复制def stock_price_stream():
while True:
yield get_latest_price() # 假设这是获取最新股价的函数
time.sleep(1)
# 消费者可以这样使用
for price in stock_price_stream():
if price > threshold:
send_alert()
这里生成器无限产生数据,而消费者按自己的节奏处理。如果没有生成器,我们就需要实现复杂的回调机制或线程通信。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Yield关键字的魔法机制
yield是生成器的核心,但它的行为常常让人困惑。本质上,yield使函数变成了一个可以暂停和恢复的状态机。
2.1 函数执行流程详解
看这个简单例子:
python复制def countdown(n):
print("Starting countdown")
while n > 0:
yield n
n -= 1
print("Blast off!")
cd = countdown(3)
print(next(cd)) # 输出:Starting countdown 然后 3
print(next(cd)) # 输出:2
print(next(cd)) # 输出:1
print(next(cd)) # 输出:Blast off! 然后抛出StopIteration
每次调用next(),函数执行到yield处暂停,保存所有局部变量状态;下次next()时从yield之后继续。最后一次调用触发StopIteration,这是for循环能自动处理的信号。
2.2 生成器的四种状态
生成器对象有明确的生命周期状态:
- CREATED:调用生成器函数但未开始执行
- RUNNING:正在执行生成器代码
- SUSPENDED:在yield处暂停
- CLOSED:执行结束或显式关闭
可以通过inspect模块检查状态:
python复制import inspect
gen = countdown(3)
print(inspect.getgeneratorstate(gen)) # GEN_CREATED
next(gen)
print(inspect.getgeneratorstate(gen)) # GEN_SUSPENDED
list(gen) # 消耗剩余值
print(inspect.getgeneratorstate(gen)) # GEN_CLOSED
理解这些状态对调试复杂生成器至关重要。
3. 生成器的进阶用法
3.1 双向通信:send()方法
生成器不仅可以产出值,还能接收值:
python复制def accumulator():
total = 0
while True:
value = yield total
if value is None: break
total += value
acc = accumulator()
next(acc) # 启动生成器
print(acc.send(10)) # 输出:10
print(acc.send(20)) # 输出:30
acc.close()
send()方法将值传入生成器,该值成为对应yield表达式的结果。注意第一次必须用next()或send(None)启动生成器。
3.2 异常处理:throw()
可以从外部向生成器注入异常:
python复制def resilient_gen():
try:
while True:
try:
yield "正常运作"
except ValueError:
yield "处理了ValueError"
except GeneratorExit:
yield "不能在这里yield!" # 这行实际上不会执行
rg = resilient_gen()
print(next(rg)) # 正常运作
print(rg.throw(ValueError)) # 处理了ValueError
rg.close()
这在测试错误处理路径时非常有用。注意GeneratorExit是当生成器被垃圾回收或显式关闭时抛出的特殊异常。
3.3 yield from:生成器委派
Python 3.3引入的yield from语法极大简化了生成器的组合:
python复制def chain(*iterables):
for it in iterables:
yield from it
list(chain([1,2], (3,4), "ab")) # [1, 2, 3, 4, 'a', 'b']
这等价于手动迭代每个可迭代对象并yield每个元素。yield from还能建立调用者和子生成器间的双向通道。
4. 生成器的实际应用场景
4.1 大数据处理管道
生成器特别适合构建数据处理管道。例如日志分析:
python复制def read_logs(file):
with open(file) as f:
for line in f:
yield line
def filter_errors(logs):
for line in logs:
if "ERROR" in line:
yield line
def extract_timestamps(logs):
for line in logs:
yield line.split()[0]
# 组合成管道
logs = read_logs("server.log")
errors = filter_errors(logs)
timestamps = extract_timestamps(errors)
for ts in timestamps:
print(ts)
每个处理步骤都是惰性的,只有最终消费时才会实际执行。这种架构可以处理远大于内存的数据集。
4.2 无限序列表示
生成器能表示数学上的无限序列:
python复制def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
传统方法无法实现这一点,因为内存是有限的。生成器让我们可以操作概念上的无限集合。
4.3 状态机实现
生成器是轻量级状态机的理想实现方式:
python复制def traffic_light():
while True:
yield "红灯"
time.sleep(5)
yield "黄灯"
time.sleep(2)
yield "绿灯"
time.sleep(10)
light = traffic_light()
print(next(light)) # 红灯
time.sleep(5)
print(next(light)) # 黄灯
相比类实现的状态模式,生成器版本更简洁直观,状态转换逻辑一目了然。
5. 性能优化与陷阱
5.1 生成器表达式 vs 列表推导式
生成器表达式(genexp)语法类似列表推导式,但返回生成器:
python复制# 列表推导式
sum([x*x for x in range(1000000)]) # 先创建百万元素的列表
# 生成器表达式
sum(x*x for x in range(1000000)) # 每次只计算一个值
在只需要迭代一次的场合,生成器表达式通常更高效。但如果需要多次访问数据,列表可能更好,因为生成器是一次性的。
5.2 常见错误模式
错误1:重复使用已耗尽的生成器
python复制numbers = (x for x in range(3))
print(list(numbers)) # [0, 1, 2]
print(list(numbers)) # [] 第二次是空的!
错误2:在生成器内修改外部状态
python复制count = 0
def counter():
global count
while count < 5:
yield count
count += 1
print(list(counter())) # [0,1,2,3,4]
print(count) # 5 - 副作用!
更好的做法是将状态封装在生成器内部。
5.3 何时不适合用生成器
- 需要随机访问元素时(生成器是单向的)
- 需要多次遍历数据时(生成器只能消费一次)
- 需要立即计算所有结果时(如构建GUI进度条)
在这些情况下,常规列表或缓存结果可能更合适。
