Python 开发技巧 · 生成器(Generator)进阶 —— 从 yield 到 yield from,写出省内存的生产者-消费者模式

一、知识点简介

生成器是 Python 最被低估的高级特性之一。很多人只把它当"惰性迭代器"用,实际上 send()/throw()/close() 让生成器拥有了双向通信 能力,yield from 则提供了生成器委托/协程组合的能力。掌握这些,你可以在处理大数据流、实现协程、构建管道模式时写出极其优雅且节省内存的代码。

二、核心概念速览

机制 作用
yield x 产出 x,暂停执行,保留状态
gen.send(val) 从外部向生成器内部发送值,val 会成为 yield 表达式的返回值
gen.throw(exc) 在生成器暂停处抛出异常
gen.close() 在暂停处抛出 GeneratorExit,终止生成器
yield from sub_gen 委托给子生成器,自动传递 send/throw/close

三、核心代码 & 使用示例

1. 基础:省内存的大文件逐行处理

python 复制代码
def read_large_file(filepath: str):
    """逐行读取,任何时候只保留一行在内存"""
    with open(filepath, encoding="utf-8") as f:
        for line in f:
            yield line.strip()

# 使用(100MB 文件也毫无压力)
for line in read_large_file("big_log.txt"):
    if "ERROR" in line:
        print(line)

2. 进阶:用 send() 实现双向通信 ------ 简易累加器

python 复制代码
def running_average():
    """动态累加器:外部边送数据边拿当前均值"""
    total, count = 0.0, 0
    average = None
    while True:
        value = yield average          # yield 产出上次均值,暂停等待 send
        total += value
        count += 1
        average = total / count

avg = running_average()
next(avg)                             # 启动生成器(走到第一个 yield)

print(avg.send(10))   # 10.0
print(avg.send(20))   # 15.0
print(avg.send(30))   # 20.0
print(avg.send(40))   # 25.0

⚠️ 第一次必须先 next(gen)gen.send(None) 将生成器推进到第一个 yield,然后才能 send 真正的值。

3. 实战:生产者-消费者协程模式

python 复制代码
def consumer():
    """消费者:不断接收数据并处理"""
    items = []
    while True:
        item = yield
        if item is None:          # 哨兵值,终止
            break
        items.append(item)
        print(f"[消费] {item}")
    return items

def producer(data, consumer_gen):
    """生产者:发送数据给消费者"""
    next(consumer_gen)            # 启动
    for item in data:
        consumer_gen.send(item)
    consumer_gen.send(None)       # 发送终止信号

# 运行
c = consumer()
producer(["A", "B", "C", "D"], c)
# 输出:
# [消费] A
# [消费] B
# [消费] C
# [消费] D

4. yield from ------ 生成器委托,优雅组合

python 复制代码
def sub_gen_a():
    yield "A1"
    yield "A2"

def sub_gen_b():
    yield "B1"
    yield "B2"

def main_gen():
    yield "START"
    yield from sub_gen_a()      # 委托:自动转发 send/throw/close
    yield "MIDDLE"
    yield from sub_gen_b()
    yield "END"

print(list(main_gen()))
# ['START', 'A1', 'A2', 'MIDDLE', 'B1', 'B2', 'END']

yield from 的真正威力:它会自动将 send() / throw() / close() 转发到子生成器,让外部代码可以直接与最内层生成器通信,无需手动传递。

5. 实战:流水线(Pipeline)模式处理数据流

python 复制代码
def lines(filepath):
    with open(filepath) as f:
        yield from f

def filter_lines(lines_gen, keyword):
    for line in lines_gen:
        if keyword in line:
            yield line

def parse_json_lines(lines_gen):
    import json
    for line in lines_gen:
        yield json.loads(line)

def extract_field(json_gen, field):
    for obj in json_gen:
        yield obj.get(field)

# 组装管道(零拷贝,全程惰性求值)
pipe = extract_field(
    parse_json_lines(
        filter_lines(
            lines("data.jsonl"),
            "ERROR"
        )
    ),
    "timestamp"
)

for ts in pipe:
    print(f"错误时间: {ts}")

每条管道节点都是一个生成器,数据逐条流过,内存占用 = 一条记录的大小,与文件总大小无关。

6. 生成器表达式 vs 列表推导 ------ 内存对比

python 复制代码
import sys

list_comp = [x ** 2 for x in range(10_000_000)]   # 立即构建全部
gen_expr  = (x ** 2 for x in range(10_000_000))    # 惰性,不占内存

print(f"列表推导: {sys.getsizeof(list_comp) / 1024 / 1024:.1f} MB")  # ~305 MB
print(f"生成器:   {sys.getsizeof(gen_expr)} bytes")                   # ~112 字节

四、注意事项 / 避坑指南

  1. 生成器只能遍历一次 ------ 第二次遍历为空。如果需要复用,用 list() 转列表或用 itertools.tee()
  2. 必须 next() 启动 ------ 带 send() 的生成器第一个 send() 必须传 None,否则抛 TypeError
  3. 异常处理 ------ 生成器内部抛出的异常会向上冒泡到调用方,可以用 gen.throw() 测试生成器的异常处理能力。
  4. 关闭生成器 ------ 不再使用时可调用 gen.close() 释放资源,生成器内可用 try/finally 做清理。
  5. 不要混用 returnyield ------ 从 Python 3.3+ 开始生成器 return 的值会附加到 StopIteration.value,需通过 yield from 捕获。
python 复制代码
def gen_with_return():
    yield 1
    yield 2
    return "完成"

g = gen_with_return()
print(list(g))  # [1, 2] ------ return 值被忽略!

# 正确获取 return 值的方式
def wrapper():
    result = yield from gen_with_return()
    print(f"子生成器返回: {result}")

list(wrapper())  # 输出: 子生成器返回: 完成

五、总结

生成器远不止是"省内存的迭代器":

  • send() ------ 让外部和生成器双向对话,实现协程模式
  • yield from ------ 优雅组合生成器,自动转发消息
  • 流水线模式 ------ 链式处理大数据流,每条记录在管道中逐级流过

下次面对大量数据处理或需要生产者-消费者模式的场景,用生成器代替先组装列表再处理的写法,不仅代码更优雅,内存性能也会显著提升。

相关推荐
geovindu1 小时前
CSharp: Dijkstra Algorithms
开发语言·后端·算法·c#
GEO_ai_zhijian1 小时前
口碑好的工业GEO优秀企业
python·搜索引擎
mikejahn1 小时前
解析ISO网站单个标准的静态网址
python
掘金者阿豪2 小时前
听书别再被会员和广告打断:把NAS变成自己的私人有声图书馆
后端
Java面试题总结2 小时前
Python 开发技巧 · 高级装饰器 —— 从基础到工业级实战
开发语言·python
纸小铭3 小时前
[MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志
python·中间件·flask
拉长的苗条3 小时前
细说 ASP.NET Cache 及其高级用法
后端·asp.net
一路向北North3 小时前
Spring Security OAuth2.0(19):JWT令牌
java·后端·spring
aiqianji3 小时前
有哪些支持长文的AI生成短篇小说软件?
人工智能·python