该用 generator 而非 list 的典型场景是内存敏感时(如处理超大日志或百万级数据库记录),因其惰性求值可避免 OOM;需确保下游直接迭代,禁用 list() 展开、len() 等破坏惰性的操作。

什么时候该用 generator 而不是 list
内存敏感场景下,比如读取超大日志文件、处理数百万条数据库记录,直接用 list 会一次性把全部数据加载进内存,容易 OOM。此时必须用 generator —— 它只在每次 next() 或循环中才产出一个值。
常见错误是误以为“写个 yield 就算惰性了”,结果在调用时又用 list(gen) 全部展开,等于白做。
- 正确做法:让下游消费逻辑直接迭代
generator,比如传给csv.writer.writerows()、pandas.read_csv(..., chunksize=...)的迭代器,或自定义的流式处理函数 - 反模式:把
generator包一层list()、map()后再转回list,或者用len()/index()等需要随机访问的操作 - 注意
itertools.chain()、itertools.islice()这类函数返回的仍是惰性对象,可安全组合;但itertools.groupby()要求输入已排序且不能 rewind,实际使用中容易因多次迭代而失效
functools.lru_cache 不是万能的惰性缓存
lru_cache 缓存的是函数调用结果,不是“延迟计算”本身。它适合纯函数、参数可哈希、结果复用率高的场景(如递归斐波那契、配置解析),但不解决“要不要算”的问题,只解决“算过就别再算”。
典型误用:给 IO 函数(如 fetch_user_from_api(user_id))加 @lru_cache,却忽略缓存穿透、过期、并发刷新等问题。
立即学习“Python免费学习笔记(深入)”;
- 缓存键完全依赖参数值,若参数含
dict、list等不可哈希类型,会直接报TypeError: unhashable type - 默认不支持异步函数,
async def需换用async_lru或手动实现 - 缓存大小设为
None可能导致内存持续增长,尤其在用户 ID 类参数无限增长时
用 __getitem__ + __len__ 实现惰性序列比 generator 更灵活
当需要支持索引访问(data[1000])、切片(data[10:20])、长度查询(len(data))时,generator 天然不支持,得退回到自定义类。
这种模式常见于机器学习的数据集封装(如 PyTorch 的 Dataset)、远程分页 API 的本地视图、或大矩阵的按需加载。
- 关键点:重载
__getitem__里不做预加载,而是根据idx动态读取/计算单条数据;__len__可返回预估总数(如 API 的 total 字段),不必真遍历 - 避免在
__init__中加载全部元数据,除非必要;例如从 S3 列出所有文件名再初始化,不如首次__getitem__时懒加载并缓存已见文件列表 - 如果要支持切片,
__getitem__接收slice对象后应返回新实例(而非list),保持惰性链路不断
异步场景下 async generator 是唯一正解
同步 generator 遇到 await 就崩,比如边请求 API 边 yield 结果,必须用 async def + yield(即 async generator),返回 AsyncGenerator 类型。
很多人卡在“怎么在普通 for 循环里用”,答案是不能——必须用 async for,且调用方也得是协程。
- 错误示例:
for item in async_gen:→TypeError: 'async_generator' object is not iterable - 正确用法:
async for item in async_gen:,且该代码块必须位于async def内;外层驱动要用asyncio.run()或事件循环显式调度 - 与
asyncio.StreamReader、aiohttp.ClientResponse.content等流式 IO 配合最自然,但要注意背压控制:如果消费者处理慢,生产者可能被挂起,需用asyncio.Queue做缓冲
真正难的不是写出惰性逻辑,而是判断哪一层该惰性、哪一层必须提前物化——比如日志解析可以惰性,但错误告警必须实时触发,中间一旦加了缓存层或批处理,就可能丢事件。









