Python是否优化尾递归?

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

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,或者我只是需要以不同的方式定义它?

106024 次浏览

CPython的不支持,也可能永远不会支持基于该主题的吉多·范·罗森的语句的尾部调用优化。

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

不,它永远不会,因为Guido van Rossum更喜欢能够有适当的回溯:

尾递归消去 (2009-04-22)

对尾部呼叫的最后发言 . (2009-04-27)

你可以用这样的转换手动消除递归:

>>> def trisum(n, csum):
...     while True:                     # Change recursion to a while loop
...         if n == 0:
...             return csum
...         n, csum = n - 1, csum + n   # Update parameters instead of tail recursion


>>> trisum(1000,0)
500500

Guido的单词在http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

我最近在我的Python历史博客上发表了一篇关于Python起源的文章 Python的函数特性。关于不支持尾巴的附加说明 递归消除(TRE)立即引发了一些评论 真遗憾,Python没有这样做,包括链接 其他人最近的博客文章试图“证明”TRE可以被添加 轻松地使用Python。所以让我来为我的立场辩护(我不这样认为 想在语言中使用TRE)。如果你想要一个简短的答案,很简单 unpythonic。这里是长答案:

我发布了一个执行尾部调用优化(处理尾部递归和延续传递样式)的模块:https://github.com/baruchel/tco

优化Python中的尾递归

经常有人声称尾递归不适合python的编码方式,人们不应该关心如何将它嵌入到循环中。我不想和你争论 这种观点;然而,有时我喜欢尝试或实施新想法 作为尾递归函数,而不是循环,因为各种原因(重点是 想法而不是过程,在我的屏幕上同时有20个简短的功能 时间,而不是只有三个“Pythonic”函数,在交互式会话中工作,而不是编辑我的代码,等等) 在Python中优化尾递归实际上非常容易。虽然据说这是不可能的 或者非常棘手,我认为它可以通过优雅、简短和通用的解决方案来实现;我甚至 我认为大多数解决方案都没有使用Python的特性。 干净的lambda表达式与非常标准的循环一起工作,可以快速,高效和 实现尾递归优化的完全可用的工具 出于个人方便,我写了一个小模块来实现这样的优化 通过两种不同的方式。我想在这里讨论一下我的两个主要功能

简单的方法:修改Y组合子

Y combinator是众所周知的;它允许在递归中使用lambda函数 方式,但它本身不允许在循环中嵌入递归调用。λ 单靠微积分是做不到这一点的。不过,Y组合子有一点变化 可以保护递归调用被实际求值。

下面是著名的Y组合子表达式:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))

稍微改变一下,我就可以得到:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))

函数f现在返回一个执行 完全相同的调用,但由于它返回它,计算可以稍后从外部完成

我的代码是:

def bet(func):
b = (lambda f: (lambda x: x(x))(lambda y:
f(lambda *args: lambda: y(y)(*args))))(func)
def wrapper(*args):
out = b(*args)
while callable(out):
out = out()
return out
return wrapper

该函数可以通过以下方式使用;下面是两个尾部递归的例子 阶乘和Fibonacci的版本:

>>> from recursion import *
>>> fac = bet( lambda f: lambda n, a: a if not n else f(n-1,a*n) )
>>> fac(5,1)
120
>>> fibo = bet( lambda f: lambda n,p,q: p if not n else f(n-1,q,p+q) )
>>> fibo(10,0,1)
55

显然,递归深度不再是一个问题:

>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42

这当然是该函数的唯一真正目的。

只有一件事不能用这个优化:它不能与a一起使用 尾递归函数求值到另一个函数(这来自于一个事实 返回的可调用对象都作为进一步的递归调用处理 没有区别)。因为我通常不需要这样的功能,我很高兴 使用上面的代码。但是,为了提供一个更通用的模块,我认为 为了找到一些解决这个问题的方法(见下一节)。

关于这个过程的速度(这不是真正的问题),它发生了 相当好;尾部递归函数的计算速度甚至比 下面的代码使用更简单的表达式:

def bet1(func):
def wrapper(*args):
out = func(lambda *x: lambda: x)(*args)
while callable(out):
out = func(lambda *x: lambda: x)(*out())
return out
return wrapper
我认为计算一个表达式,即使很复杂,也比 求几个简单表达式的值,这是第二个版本中的情况。 我没有在我的模块中保留这个新函数,而且我没有看到它在任何情况下

带有异常的延续传递样式

这是一个更一般的函数;它能够处理所有的尾递归函数, 包括那些返回其他函数的。的递归调用被识别 使用异常返回的其他返回值。这个解比 前一个;可以使用一些特殊的方法来编写更快的代码 在主循环中检测到作为“标志”的值,但我不喜欢的想法 使用特殊值或内部关键字。有一些有趣的解释 使用异常:如果Python不喜欢尾部递归调用,则异常 当尾递归调用确实发生时,应该被引发,而python的方式将是 捕捉异常以便找到一些干净的解决方案,这实际上是 这里发生了…< / p >
class _RecursiveCall(Exception):
def __init__(self, *args):
self.args = args
def _recursiveCallback(*args):
raise _RecursiveCall(*args)
def bet0(func):
def wrapper(*args):
while True:
try:
return func(_recursiveCallback)(*args)
except _RecursiveCall as e:
args = e.args
return wrapper

现在可以使用所有函数。在下面的例子中,f(n)被求值为 n为任意正值时的恒等函数:

>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42
当然,可以认为异常并不是故意使用的 重定向解释器(作为一种goto语句或更确切地说是一种 我不得不承认这一点。但是,再一次, 我觉得使用try和一行return语句的想法很有趣:我们尝试返回 某些(正常行为),但由于发生递归调用(异常),我们不能这样做

初始回答(2013-08-29)。

我写了一个非常小的插件来处理尾部递归。你可以在我的解释中找到它:https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

它可以将一个使用尾递归风格编写的lambda函数嵌入到另一个函数中,该函数将其作为循环计算。

在我看来,这个小函数最有趣的特点是,这个函数不依赖于一些肮脏的编程技巧,而仅仅是lambda微积分:当插入另一个lambda函数时,函数的行为会改变为另一个,它看起来非常像Y组合子。

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

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

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

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