我注意到,在我的机器上,下面的代码达到了n = 2960的深度递归的最大值:
m = {0:0, 1:1}
def f(n):
if n not in m:
m[n] = f(n - 1) + f(n - 2)
return m[n]
而这个版本对于n = 988达到它:
m = {0:0, 1:1}
def f(n):
if n not in m:
m[n] = sum(f(n - i) for i in [1, 2])
return m[n]
有谁能解释一下“引擎盖下”发生了什么来解释这种差异吗?
更确切地说,如果能理解为什么这个例子中的因子是3,并且能够推导出更多项的求和,那将是非常好的。
3条答案
按热度按时间s4n0splo1#
TL;DR:
sum
是一个额外的函数调用,它所求和的生成器表达式也是用嵌套函数作用域(docs)实现的...解析是在一个单独的隐式嵌套作用域中执行的。这确保了在目标列表中赋值的名称不会“泄漏”到封闭作用域中。
因此,第二种方法在递归过程中使用了 * 两个 * 额外的堆栈帧,以及递归调用本身,这解释了这里的因子3。
请注意,默认的递归限制是1000,所以对于第一种情况,你应该会看到堆栈溢出正好是1000,而对于第二种情况(在Python 3.10或更低版本上),堆栈溢出是334。要在这里得到2960和988,你可能需要:
sys.setrecursionlimit(3000)
的东西,然后例如,在IDE或精心设计的交互式REPL中运行这段代码,很可能已经完成了上述两项工作。
有趣的最新进展:
有一个PEP 709 – Inlined comprehensions,它是关于从解析中删除这个隐藏函数的。生成器表达式目前没有内联在PEP的参考实现中,尽管它可能会在将来被考虑。
CPython 3.11已经做了一些优化,以减少这段代码中的函数调用次数,这改变了堆栈溢出的点。它将发生在501而不是CPython 3.11中的334。原因在 * Python 3.11中的新功能的更新日志中描述:内联Python函数调用 *。
webghufk2#
我也会在这里添加我的调试。在运行一些测试之后,我发现
traceback
模块给我的调用堆栈明显(~333)小于递归限制。我注意到这个差异总是接近调用堆栈上<genexpr>
调用的数量:以下是一些
sum_every_n
值的结果:看起来生成器确实向堆栈添加了一个函数调用,但每个生成器表达式也算作调用堆栈上的两个函数。这解释了你最初问题中的比率。虽然不知道为什么会这样,但我欢迎任何可能的解释!
z9zf31ra3#
这是一个有趣的行为,我没有找到问题的根源,但我仍然想分享一些我写的日志代码,可能会帮助其他人找到问题:
输出为:
我的Python版本是3.8.15,你应该尝试运行这段代码,看看输出是否与你的Python版本相同。
在我的Python版本中,我达到了
f(333)
和g(997)
的递归限制编辑:感谢John Kugelman,我越来越接近答案了,代码如下:
输出为:
所以问题是生成器表达式被放在调用堆栈上,占用了实际递归函数可以使用的空间。现在研究一下为什么生成器表达式被放在堆栈上,就像它是一个函数一样,这将是很有趣的。