
@cache 会缓存生成器对象本身,而非每次调用重新创建;多次调用返回同一生成器实例,导致后续迭代因状态耗尽而无输出——这是递归生成器被缓存后结果异常的根本原因。
`@cache` 会缓存生成器对象本身,而非每次调用重新创建;多次调用返回同一生成器实例,导致后续迭代因状态耗尽而无输出——这是递归生成器被缓存后结果异常的根本原因。
在 Python 中,functools.cache(或 lru_cache)通过记忆化(memoization)提升函数性能,其原理是将函数调用的参数映射到返回值并缓存。但对于生成器函数(即含 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)) # [] —— 已耗尽,无任何产出!回到原始代码中的 Blinking 函数:它是一个深度递归生成器,依赖参数 (v, i, N) 生成一系列值。当 @cache 应用于其上时,首次调用 Blinking(0, 0, 25) 返回一个生成器 g;后续递归调用(如 Blinking(1, 1, 25) 或 Blinking(...))若参数重复,就会复用此前缓存的生成器——但该生成器可能已被上游 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) # 得到预期计数⚠️ 关键注意事项:
-
@cache与生成器函数天然不兼容:生成器是一次性资源,缓存它等于缓存“已执行一半的状态”。 - 若必须保留生成器流式特性(如处理超大数据集避免内存爆炸),可考虑手动实现带状态的缓存(如
dict存储(v,i,N) → list),但需权衡内存开销。 -
yield from本身无副作用,问题根源始终在于缓存了不可重入的生成器对象。
总结:@cache 是为幂等、无状态函数设计的;生成器函数本质上是有状态的迭代器工厂。混淆二者会导致隐蔽的逻辑错误。牢记——缓存的是生成器对象,不是生成逻辑。

















