我有下面的一段代码,失败的错误如下:
RuntimeError:超出最大递归深度
我尝试重写这个代码,以允许尾部递归优化(TCO)。我相信,如果进行了TCO,那么该代码应该是成功的。
def trisum(n, csum):
if n == 0:
return csum
else:
return trisum(n - 1, csum + n)
print(trisum(1000, 0))
我是否应该得出结论,Python不做任何类型的TCO,或者我只是需要以不同的方式定义它?
在Python中,尾调用永远不能优化为跳转。优化是一种保留程序含义的程序转换。尾调用消除并不能保留Python程序的含义。
经常提到的一个问题是,尾部调用消除会改变调用堆栈,而Python允许运行时对堆栈进行内省。但还有一个问题很少被提及。在野外可能有很多这样的代码:
def map_file(path):
f = open(path, 'rb')
return mmap.mmap(f.fileno())
对mmap的调用。Mmap在尾部位置。如果它被跳转所取代,那么当前堆栈帧将在控制传递给mmap之前被丢弃。当前堆栈帧包含对文件对象的唯一引用,因此文件对象可以(在CPython中)在mmap被调用之前被释放,这将关闭文件描述符,在mmap看到它之前使其失效。
在最好的情况下,代码将失败并出现异常。在最坏的情况下,文件描述符可能在另一个线程中重用,导致mmap映射错误的文件。因此,这种“优化”对于大量现有的Python代码来说可能是灾难性的。
Python规范保证了这样的问题不会发生,所以可以肯定没有符合规范的实现将return f(args)转换为跳转——除非它有一个复杂的静态分析引擎,可以证明在这种情况下,早期丢弃一个对象不会产生可观察到的结果。
所有这些都不会阻止Python为带有跳转语义的显式尾部调用添加语法,例如
return from f(args)
这不会破坏没有使用它的代码,而且它可能对自动生成的代码和一些算法有用。GvR不再是BDFL,所以它可能会发生,但我不会屏住呼吸。
Python中没有内置的尾部递归优化。但是,我们可以通过抽象语法树(AST)“重建”函数,消除那里的递归,并用循环替换它。Guido是绝对正确的,这种方法有一些局限性,所以我不推荐使用。
然而,我仍然编写了我自己的优化器版本(而不是作为一个训练示例),您甚至可以尝试它是如何工作的。
通过pip安装此包:
pip install astrologic
现在你可以运行这个示例代码:
from astrologic import no_recursion
counter = 0
@no_recursion
def recursion():
global counter
counter += 1
if counter != 10000000:
return recursion()
return counter
print(recursion())
这个解决方案不稳定,您永远不应该在生产中使用它。你可以在github的页面上阅读一些重要的限制(俄语,抱歉)。然而,这个解决方案是相当“真实”的,没有中断代码和其他类似的技巧。
在Python中,尾调用永远不能优化为跳转。优化是一种保留程序含义的程序转换。尾调用消除并不能保留Python程序的含义。
经常提到的一个问题是,尾部调用消除会改变调用堆栈,而Python允许运行时对堆栈进行内省。但还有一个问题很少被提及。在野外可能有很多这样的代码:
def map_file(path):
f = open(path, 'rb')
return mmap.mmap(f.fileno())
对mmap的调用。Mmap在尾部位置。如果它被跳转所取代,那么当前堆栈帧将在控制传递给mmap之前被丢弃。当前堆栈帧包含对文件对象的唯一引用,因此文件对象可以(在CPython中)在mmap被调用之前被释放,这将关闭文件描述符,在mmap看到它之前使其失效。
在最好的情况下,代码将失败并出现异常。在最坏的情况下,文件描述符可能在另一个线程中重用,导致mmap映射错误的文件。因此,这种“优化”对于大量现有的Python代码来说可能是灾难性的。
Python规范保证了这样的问题不会发生,所以可以肯定没有符合规范的实现将return f(args)转换为跳转——除非它有一个复杂的静态分析引擎,可以证明在这种情况下,早期丢弃一个对象不会产生可观察到的结果。
所有这些都不会阻止Python为带有跳转语义的显式尾部调用添加语法,例如
return from f(args)
这不会破坏没有使用它的代码,而且它可能对自动生成的代码和一些算法有用。GvR不再是BDFL,所以它可能会发生,但我不会屏住呼吸。