如何在递归生成器中正确使用@cache装饰器与yield from
时间:2026-08-15 | 作者:清风无痕 | 阅读:0@cache 会缓存生成器对象本身,而不是每次调用时重新创建生成器。
多次调用会返回同一生成器实例。后续迭代时,如果该实例已经被消费,就会因状态耗尽而无输出。
这正是递归生成器被缓存后结果异常的根本原因。
@cache 与生成器冲突的本质
@cache 会缓存生成器对象本身,而非每次调用重新创建;多次调用返回同一生成器实例,导致后续迭代因状态耗尽而无输出——这是递归生成器被缓存后结果异常的根本原因。
在 Python 里,functools.cache 或 lru_cache 的提速原理,在于“记住”结果。
也就是把函数调用参数和对应返回值做映射并缓存起来。
但生成器函数,也就是带 yield 的函数,返回的并不是最终结果本身。
它返回的是一个惰性求值、且只能消费一次的生成器对象,也就是 generator 实例。
问题就在这里:如果缓存里存的是这个生成器,那么之后传入相同参数时,拿到的就会始终是同一个生成器。
而这个生成器,可能已经被消费了一部分,甚至已经彻底耗尽。于是,各种意料之外的行为就会出现。
示例:为什么第二次遍历没有结果
下面这个示例可以直接说明问题本质:
from functools import cache @cache def example_gen(): yield 1 yield 2 yield 3 g1 = example_gen()# 第一次调用:创建新生成器 g2 = example_gen()# 第二次调用:返回缓存的同一生成器对象 print(g1 is g2)# True —— 它们是同一个对象 print(list(g1))# [1, 2, 3] —— 正常遍历 print(list(g2))# [] —— 已耗尽,无任何产出!
- 第一次调用:得到一个新的生成器对象。
- 第二次调用:命中缓存,返回同一个生成器对象。
- 结果:
g1 is g2为True,说明两者本质上是同一个实例。 - 后果:
g1被遍历完后,g2也随之耗尽。
原始递归场景中的问题所在
再看原始代码里的 Blinking 函数。
本质上,它是一个深度递归的生成器,围绕参数 (v, i, N) 持续产出一串值。
问题恰恰出在加上 @cache 之后。
第一次调用 Blinking(0, 0, 25) 时,返回的是一个生成器对象 g。
而后续递归调用,比如 Blinking(1, 1, 25) 或其他参数组合,一旦命中相同参数,就会直接复用之前缓存下来的那个生成器。
麻烦在于,生成器是有消费状态的。
它很可能已经被上层的 yield from 提前取走了一部分,甚至全部元素。
这样一来,下面的子调用表面上看是命中了缓存,实际上拿到的却是一个已经被消费过的生成器。
最终结果就是:产出会“悄无声息”地变少,最后让计数 res 明显偏低。
正确解法:避免缓存生成器函数本身
若需优化递归生成器性能,应将纯计算逻辑(返回具体值)与生成器逻辑分离。
只对纯函数做缓存,不要直接缓存生成器函数。
from functools import cache # 缓存纯计算函数(返回确定性结果) @cache def _blink_value(v, i, N): """返回该节点应产生的所有终态值列表(非生成器)""" if i >= N: return [v] elif v == 0: return _blink_value(1, i + 1, N) elif len(str(v)) % 2 == 0: s = str(v) left = int(s[:len(s)//2]) right = int(s[len(s)//2:]) return _blink_value(left, i + 1, N) + _blink_value(right, i + 1, N) else: return _blink_value(v * 2024, i + 1, N) # 生成器仅负责包装,不参与缓存 def Blinking(v, i, N): for val in _blink_value(v, i, N): yield val # 使用方式不变,但结果正确且可缓存 res = sum(1 for _ in Blinking(0, 0, 25)) print(res)# 得到预期计数
为什么这种方式有效
- 可缓存的是结果:
_blink_value返回的是确定性的具体值,而不是一次性迭代器。 - 生成器只做包装:
Blinking每次调用都会重新生成可迭代流程。 - 避免状态污染:不会复用已经被消费过的生成器对象。
关键注意事项
@cache与生成器函数天然不兼容:生成器是一次性资源,缓存它等于缓存“已执行一半的状态”。- 若必须保留生成器流式特性(如处理超大数据集避免内存爆炸),可考虑手动实现带状态的缓存(如
dict存储(v,i,N) → list),但需权衡内存开销。 yield from本身无副作用,问题根源始终在于缓存了不可重入的生成器对象。
总结
@cache 是为幂等、无状态函数设计的。
而生成器函数本质上是有状态的迭代器工厂。
混淆二者,会导致隐蔽的逻辑错误。
牢记:缓存的是生成器对象,不是生成逻辑。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- 迅捷路由器怎么调信号最强,设置时要注意什么?
- 时间:2026-08-27
-
- vivo浏览器怎么卸不掉?原因和解决方法在这里
- 时间:2026-08-27
-
- OPPO R11s黑屏了,怎么强制恢复出厂设置?
- 时间:2026-08-27
-
- 飞利浦显示器包装盒有生产日期和保修期吗?怎么看?
- 时间:2026-08-27
-
- 联想新平板开机必须联网吗?怎么做?
- 时间:2026-08-27
-
- 平板横竖屏切换设置与问题解决
- 时间:2026-08-27
-
- 移动电源容量怎么测?要准备哪些工具?
- 时间:2026-08-27
-
- 荣耀90 Pro防水吗?防水级别多少?怎么用才安全
- 时间:2026-08-27
精选合集
更多大家都在玩
大家都在看
更多-
- 2026年9月17日小鸡庄园答案
- 时间:2026-09-16
-
- 蚂蚁庄园今日答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园小课堂今日最新答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园小鸡答题今日答案2026年9月17日
- 时间:2026-09-16
-
- 褪黑素主要由人体哪个器官分泌 蚂蚁庄园今日答案9.17
- 时间:2026-09-16
-
- 蚂蚁庄园今天答题答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园答题今日答案2026年9月17日
- 时间:2026-09-16
-
- 研学旅游指导师的核心服务对象是 蚂蚁新村今日答案2026.9.16
- 时间:2026-09-16
