我有下面的一段代码,失败的错误如下:

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的页面上阅读一些重要的限制(俄语,抱歉)。然而,这个解决方案是相当“真实”的,没有中断代码和其他类似的技巧。

基于Guido van Rossum关于该主题的陈述,CPython现在不支持,将来也可能永远不会支持尾部调用优化。

我听说过这样的说法,因为它修改堆栈跟踪的方式,使得调试更加困难。

尝试实验macropy TCO实现的大小。

除了优化尾部递归,你还可以手动设置递归深度:

import sys
sys.setrecursionlimit(5500000)
print("recursion limit:%d " % (sys.getrecursionlimit()))

在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,所以它可能会发生,但我不会屏住呼吸。