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. 不要混用 return 和 yield ------ 从 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 ------ 优雅组合生成器,自动转发消息
  • 流水线模式 ------ 链式处理大数据流,每条记录在管道中逐级流过

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

相关推荐
高频因子挖掘机20 分钟前
历史行情返回顺序变了,为什么按行号计算指标很危险?
后端·github·api
xiaogai_gai1 小时前
仅查询策略实战:从金蝶云·星空旗舰版定时拉取收料通知单
大数据·数据库·python
benchmark_cc1 小时前
Python 量化系统如何设计统一的数据访问接口:从策略解耦到数据层架构
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
const_qiu2 小时前
第四章 00:开篇 —— 从「会聊天」到「记得住、会干活、能协作」
java·开发语言·人工智能·python
团子哒哒2 小时前
数组、切片和Map
后端
专注仿真2 小时前
ThinkPHP6 API接口开发实例:从零搭建RESTful风格接口
后端·php·restful
开心猴爷2 小时前
应对iOS审核4.3:可以使用 IpaGuard 来给属性名混淆的技术细节与平衡之道
后端·ios
1m555555552 小时前
RAG 工程实战(五):正确知识已经进入 Prompt,为什么大模型还是会答错?
后端·面试
易小尊2 小时前
我的模型在 65% 以上时准得可怕,在 50% 时像在掷硬币
后端
用户2788607285672 小时前
fallback 会盖掉平台提示词:system、空数组和 systemAppend 的差别
前端·后端