位置:首页 > Python > 如何在递归生成器中正确使用@cache装饰器与yield from

如何在递归生成器中正确使用@cache装饰器与yield from

时间:2026-08-15  |  作者:清风无痕  |  阅读:0

@cache 会缓存生成器对象本身,而不是每次调用时重新创建生成器。

多次调用会返回同一生成器实例。后续迭代时,如果该实例已经被消费,就会因状态耗尽而无输出。

这正是递归生成器被缓存后结果异常的根本原因。

如何正确使用 @cache 装饰器与 yield from 的递归生成器函数

@cache 与生成器冲突的本质

@cache 会缓存生成器对象本身,而非每次调用重新创建;多次调用返回同一生成器实例,导致后续迭代因状态耗尽而无输出——这是递归生成器被缓存后结果异常的根本原因。

在 Python 里,functools.cachelru_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 g2True,说明两者本质上是同一个实例。
  • 后果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 是为幂等、无状态函数设计的。

而生成器函数本质上是有状态的迭代器工厂。

混淆二者,会导致隐蔽的逻辑错误。

牢记:缓存的是生成器对象,不是生成逻辑。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多