导读:本期聚焦于桃子创作的《为什么@lru_cache会导致深度递归栈溢出而普通递归不会?》,敬请观看详情。Python 的 lru_cache 并不是把原函数直接替换掉,而是在原函数外面加了一层缓存包装器。递归函数一旦被装饰,每次递归调用都会先进入包装器,缓存未命中时再由包装器调用原函数,原函数内部的下一次递归又回到包装器。这样每一层递归路径上都多出一次包装调用:在纯 Python 实现中会多一个真实 Python 栈帧,在 CPython 的 C 加速路径中也会多出 C 层栈帧和缓存键处理。普通递归只保留用户函数自己的栈帧,因此同样的递归深度下,lru_cache 版本会占用更多调用栈空间,更早触发 RecursionError。线性递归或首次计算路径尤其明显,因为缓存很少命中,额外成本不会被命中收益抵消。解决方法包括提高递归上限、改为迭代、手动缓存或使用动态规划。

Python 标准库中的 lru_cache 能让递归函数复用已经计算过的结果,但它并不是无代价的。它会在原函数外层再包一个 wrapper,每一次缓存未命中的递归调用都必须先经过 wrapper,再由 wrapper 进入原函数。对深度递归来说,这个额外的调用层会迅速放大栈帧数量。假设普通递归某一时刻占用 N 个栈帧,使用 lru_cache 后,同样的逻辑在执行到第 N 层时,调用栈上会同时保留包装器帧和原函数帧,实际占用的栈空间接近普通递归的两倍甚至更多。栈空间是有限的,提前耗尽就表现为 RecursionError。

为什么@lru_cache会导致深度递归栈溢出而普通递归不会?

一、装饰器包装让每个递归层级多出一次调用

Python 函数调用会在调用栈上分配栈帧,栈帧里保存局部变量、参数、返回地址和临时表达式结果。递归越深,栈帧越多。Python 默认递归限制由 sys.getrecursionlimit() 控制,通常是 1000,也就是当调用栈中的帧数超过这个阈值时,解释器会抛出 RecursionError。普通递归只维护用户函数自身的栈帧,栈帧数量与递归深度基本一致。

加入 @lru_cache 后,模块里的函数名不再指向原始函数,而是指向缓存的包装函数。包装函数先做参数哈希和缓存查找,未命中时才调用原始函数。原始函数内部再调用同名函数时,又进入包装函数。于是每个递归层级至少包含一个包装函数调用和一个原始函数调用。在 CPython 的 C 加速路径中,包装层可能不直接对应 Python 栈帧,但仍会占用真实 C 调用栈;在纯 Python 实现或部分运行时中,包装层就是一个额外的 Python 帧。无论哪种情况,每层递归的实际栈消耗都比普通递归更大。

换句话说,lru_cache 并不会消除递归调用,它只是把缓存逻辑插进递归路径里。对于深层线性递归,缓存很少命中,这些包装成本不会因为复用结果而被抵消,反而会加速栈空间的耗尽。

from functools import lru_cache

# 普通版
def plain_fib(n):
    if n < 2:
        return n
    return plain_fib(n - 1) + plain_fib(n - 2)

# lru_cache 版
@lru_cache(None)
def cached_fib(n):
    if n < 2:
        return n
    return cached_fib(n - 1) + cached_fib(n - 2)

二、实验:普通递归与 lru_cache 递归的最大深度对比

为了更直观地观察差距,可以不用斐波那契而用一个线性递归函数,例如对 1 到 n 求和。线性递归不会有重复子问题,因此 lru_cache 的缓存命中率为 0,每次递归都走完整的包装调用路径,能体现最坏情况下的栈消耗。

import sys
from functools import lru_cache

sys.setrecursionlimit(2000)

def plain_sum(n):
    if n == 0:
        return 0
    return n + plain_sum(n - 1)

@lru_cache(None)
def cached_sum(n):
    if n == 0:
        return 0
    return n + cached_sum(n - 1)

for name, fn in [("plain", plain_sum), ("cached", cached_sum)]:
    max_n = 0
    for n in range(1, 2000):
        try:
            fn(n)
            max_n = n
        except RecursionError:
            break
    print(name, max_n)

在 CPython 3.11 左右的运行结果通常显示,plain_sum 可以递归到接近 sys.getrecursionlimit() 的深度,而 cached_sum 大约只能跑到该值的一半到七成左右,具体比例取决于 Python 版本和底层实现。差距来自两点:其一,每个递归层多出包装调用,实际栈占用更大;其二,包装层里还要维护缓存键、锁和缓存字典操作,单层占用的内存也比纯函数帧更大。

对于斐波那契这类重复子问题很多的递归,缓存命中后不会再进入原始函数,可能局部减少栈帧数量,但这不改变线性递归或首次计算路径上的额外开销。当递归问题是链式结构时,缓存几乎不会命中,栈深度直接受包装层拖累。因此出现 RecursionError 时,不一定是逻辑错误,而是栈消耗被装饰器放大。

三、避免方案:从递归改写、手动缓存到动态规划

最简单的方法是调用 sys.setrecursionlimit(10000) 把阈值调大。这在测试和小规模数据中有效,但属于临时方案。线程栈大小是操作系统决定的,递归限制调得过大可能触发 C 栈溢出,导致 Python 进程直接崩溃,而不是抛出可以捕获的异常。生产环境中不建议依赖过高的递归限制。

import sys
sys.setrecursionlimit(100000)

最稳妥的方法是改成迭代。大部分线性递归都可以直接转换成循环。迭代只保留固定数量的局部变量,不会随 n 增加栈帧,因此从根本上回避了递归深度问题。比如求和和斐波那契都可以写成简单循环。

def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

def sum_iter(n):
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

如果必须保留递归,可以手动维护缓存字典,减少包装层。手动缓存把字典存在函数外部或通过参数传递,递归调用仍然存在,但调用栈中只有用户函数自身,不会额外增加 lru_cache 包装帧。相比装饰器方案,同样大小的栈空间可以支持更深的递归。它的缺点是代码略繁琐,需要自己处理缓存键和并发控制。

_cache = {}

def fib_manual(n):
    if n in _cache:
        return _cache[n]
    if n < 2:
        value = n
    else:
        value = fib_manual(n - 1) + fib_manual(n - 2)
    _cache[n] = value
    return value

对于有最优子结构的问题,建议自底向上动态规划。动态规划直接按规模从小到大计算,不需要递归回溯。比如斐波那契可以用滚动变量保存中间结果。这样既得到缓存复用的好处,又没有递归栈压力。只有在递归结构天然清晰、且深度可控时,才建议保留递归加缓存的写法。

def fib_dp(n):
    if n < 2:
        return n
    prev, curr = 0, 1
    for _ in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr

lru_cache 依然是非常有用的工具,在浅层递归或重复计算密集的场景中收益明显。只要认识到它的包装层会增加栈帧数量,就能在遇到 RecursionError 时快速判断是递归逻辑问题还是装饰器带来的栈开销。深度递归场景优先选迭代或动态规划,才是更可靠的工程选择。

lru_cache深度递归栈溢出修改时间:2026-09-26 02:31:47

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0926/61953.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。