
@cache 会缓存生成器对象本身而非其产出结果,导致多次调用返回同一可耗尽的生成器,从而引发迭代异常或结果缺失。
`@cache` 会缓存生成器对象本身而非其产出结果,导致多次调用返回同一可耗尽的生成器,从而引发迭代异常或结果缺失。
在 Python 中,functools.cache(或 lru_cache)通过记忆化函数返回值来提升性能。但当它作用于生成器函数(即含 yield 的函数)时,其行为与普通函数有本质区别:它缓存的是生成器对象(generator object),而非生成器所产出的值序列。
由于生成器是一次性可迭代对象(exhaustible),一旦被遍历完毕,再次调用 next() 将抛出 StopIteration。而 @cache 使所有后续调用都返回同一个已耗尽的生成器实例,因此后续迭代将无法产生任何新值——这正是你观察到“结果数量显著降低”的根本原因。
以下代码直观展示了该问题:
from functools import cache
@cache
def demo_generator():
yield "first"
yield "second"
g1 = demo_generator()
g2 = demo_generator() # 返回缓存的 g1,而非新生成器
print(g1 is g2) # True
print(list(g1)) # ['first', 'second']
print(list(g2)) # [] —— 已耗尽,无输出!回到你的 Blinking 函数:递归中大量调用 yield from Blinking(...) 实际上反复复用同一缓存生成器,导致深层递归分支被跳过或截断,最终计数 res 远低于预期。
✅ 正确做法是避免直接缓存生成器函数。若需优化递归生成器性能,应缓存纯计算型中间结果(如子问题的返回值),再由非缓存的生成器组装输出。例如重构如下:
from functools import cache
# 缓存纯函数:返回确定性结果(如子序列长度、关键状态等)
@cache
def _blinking_count(v, i, N):
"""返回从 (v,i) 开始、剩余 N-i 步内能生成的元素总数"""
if i >= N:
return 1
elif v == 0:
return _blinking_count(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 _blinking_count(left, i + 1, N) + _blinking_count(right, i + 1, N)
else:
return _blinking_count(v * 2024, i + 1, N)
# 主生成器不加 @cache,确保每次调用创建新生成器
def Blinking(v, i, N):
if i >= N:
yield v
elif v == 0:
yield from Blinking(1, i+1, N)
elif len(str(v)) % 2 == 0:
s = str(v)
yield from Blinking(int(s[:len(s)//2]), i+1, N)
yield from Blinking(int(s[len(s)//2:]), i+1, N)
else:
yield from Blinking(v*2024, i+1, N)⚠️ 注意事项:
-
@cache仅适用于幂等、无副作用、返回相同结果的函数;生成器函数天然不满足此条件。 - 若必须缓存生成逻辑,可考虑使用
itertools.tee()复制生成器(但内存开销大),或改用列表/元组缓存全部结果(牺牲内存换复用性)。 - 对深度递归场景,还需警惕栈溢出风险,必要时转为迭代实现或增加
sys.setrecursionlimit()(谨慎使用)。
总之:生成器 ≠ 可缓存值。理解 @cache 的缓存粒度(对象级)是避免此类陷阱的关键。

















