Python 惰性计算在工程中的应用

9次阅读

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

Python 惰性计算在工程中的应用

什么时候该用 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免费学习笔记(深入)”;

  • 缓存键完全依赖参数值,若参数含 dictlist 等不可哈希类型,会直接报 TypeError: unhashable type
  • 默认不支持异步函数,async def 需换用 async_lru 或手动实现
  • 缓存大小设为 None 可能导致内存持续增长,尤其在用户 ID 类参数无限增长时

__getitem__ + __len__ 实现惰性序列比 generator 更灵活

当需要支持索引访问(data[1000])、切片data[10:20])、长度查询(len(data))时,generator 天然不支持,得退回到自定义类。

这种模式常见于机器学习的数据集封装(如 pytorchDataset)、远程分页 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.streamReaderaiohttp.ClientResponse.content 等流式 IO 配合最自然,但要注意背压控制:如果消费者处理慢,生产者可能被挂起,需用 asyncio.Queue 做缓冲

真正难的不是写出惰性逻辑,而是判断哪一层该惰性、哪一层必须提前物化——比如日志解析可以惰性,但错误告警必须实时触发,中间一旦加了缓存层或批处理,就可能丢事件。

text=ZqhQzanResources