yield 是内存优化手段而非提速魔法,将全量加载转为按需产出,可使内存占用从oom降至几kb;关键要消除中间容器,避免yield后转list、混用return或重复使用已耗尽生成器。

yield 不是“让代码变快”的魔法,而是把“一次性全装进来”变成“要的时候才拿一个”,内存压降往往不是 10%,而是从 OOM 直接降到稳定运行。
生成器函数 vs 返回列表:内存占用差一个数量级
处理大型报表时,常见错误是用 return list 把所有行、所有聚合结果塞进内存。比如导出 50 万行销售数据:
- ❌
def get_all_sales(): return [row for row in db_query()]—— 立即加载全部,可能吃掉 1.2GB 内存 - ✅
def get_all_sales(): for row in db_query(): yield row—— 每次只保留当前row,内存恒定在几 KB
关键不是“有没有用 yield”,而是“有没有把中间容器(如 list、dict)全干掉”。哪怕只在函数末尾加一句 return list(result),前面的 yield 就白写了。
yield 在分页报表导出中的实际写法
很多报表系统要求支持“导出全部”,但数据库查一次全量会超时或爆内存。正确做法是用生成器封装分页逻辑:
- 每次
yield一批(如 1000 行),而不是拼成大列表再返回 - 避免在生成器内缓存历史批次(比如用
all_rows += batch) - 数据库游标必须保持打开状态,直到生成器结束;用
with包裹连接时,确保生成器在with块内被完全消费,否则连接提前关闭会报ProgrammingError
示例片段:
def export_sales_report():
offset = 0
while True:
batch = db.execute("SELECT * FROM sales LIMIT 1000 OFFSET %s", (offset,))
if not batch:
break
for row in batch:
yield row # 不打包,不缓存
offset += 1000
别踩 yield + return 的坑
生成器函数里混用 return value 很容易误以为能“返回最终汇总值”,但实际调用方根本拿不到这个 value:
-
return "done"会触发StopIteration异常,且"done"存在异常的value属性里,next(gen)拿不到 - 想传状态?改用
yield吐出元组,比如yield ("summary", total) - 想提前终止?直接
return(无值)就行,不用写return None
更隐蔽的问题:生成器一旦被迭代完,就不可重用。报表导出中途失败后想重试?得重新调用 export_sales_report() 得到新生成器,不能对旧对象再 next()。
生成器和 pandas / openpyxl 配合的边界在哪
生成器省的是 Python 层内存,但下游库可能又把它全读进去:
-
pd.DataFrame(gen)会立刻把整个生成器转成 list —— 白省了 -
openpyxl写 Excel 时,如果用ws.append(row)逐行写,配合生成器完全 OK;但若先list(gen)再批量写,就回到老路 - 真正安全的链路是:生成器 → 自定义 CSV writer(
csv.writer(f).writerow())→ 文件流,全程无中间容器
最容易被忽略的一点:生成器的“惰性”和“一次性”绑死在一起。你没法既要它省内存,又要它支持随机访问或反复遍历 —— 那就得换数据结构,而不是硬改 yield。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











