Python优化尾递归吗?

时间:2012-11-27 19:53:41

标签: python recursion stack stack-overflow tail-recursion

我有以下代码失败并出现以下错误:

  

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

6 个答案:

答案 0 :(得分:177)

不,自从Guido更喜欢能够进行适当的追溯之后,它永远不会发生。

http://neopythonic.blogspot.com.au/2009/04/tail-recursion-elimination.html

http://neopythonic.blogspot.com.au/2009/04/final-words-on-tail-calls.html

您可以使用像这样的转换手动消除递归

>>> 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

答案 1 :(得分:147)

编辑(2015-07-02): 随着时间的推移,我的答案变得非常受欢迎,因为它最初是一个链接而不是其他任何东西,我决定花一些时间并完全重写(但最初的答案可以在最后找到)。

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

在Python中优化尾递归

经常声称尾递归并不适合pythonic编码方式 并且那个人不应该关心如何将它嵌入循环中。我不想与之争辩 这个观点;但有时我喜欢尝试或实施新想法 作为尾递归函数,而不是由于各种原因的循环(专注于 想法,而不是在过程中,在我的屏幕上有20个短的功能 时间而不仅仅是三个" pythonic"功能,在互动会话中工作 而不是编辑我的代码等。)。

在Python中优化尾递归实际上非常简单。虽然据说是不可能的 或者非常棘手,我认为可以通过优雅,简洁和通用的解决方案来实现;我什至 我认为大多数这些解决方案都不会使用Python功能。 清晰的lambda表达式与非常标准的循环一起工作,可以快速,高效地执行 完全可用的工具,用于实现尾递归优化。

为了个人方便,我写了一个实现这种优化的小模块 通过两种不同的方式。我想在这里讨论我的两个主要职能。

干净的方式:修改Y组合器

Y组合器众所周知;它允许在递归中使用lambda函数 方式,但它本身不允许在循环中嵌入递归调用。 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

该功能可以按以下方式使用;这里有两个尾递归的例子 factorial和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不喜欢尾递归调用,那就是异常 当发生尾递归调用时应该引发,并且pythonic方式将是 捕获异常以便找到一些干净的解决方案,这实际上是什么 发生在这里...

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函数,它将它作为一个循环进行评估。

在我看来,这个小函数中最有趣的特性是函数不依赖于一些简单的编程hack而只依赖于lambda演算:函数的行为在插入时改为另一个函数在另一个看起来非常像Y-combinator的lambda函数中。

问候。

答案 2 :(得分:19)

圭多的话是http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

  

我最近在我的Python历史博客中发布了一个关于其起源的条目   Python的功能特性。关于不支持尾巴的一面评论   递归消除(TRE)立即引发了几条评论   遗憾的是,Python没有这样做,包括链接   其他人最近的博客条目试图“证明”可以添加TRE   轻松地使用Python。所以,让我捍卫自己的立场(这是我没有的   希望在语言中的TRE)。如果你想要一个简短的答案,那就简单了   unpythonic。这是一个很长的答案:

答案 3 :(得分:6)

CPython不会也可能永远不会支持基于Guido关于这个主题的声明的尾调用优化。我听说过,由于它如何修改堆栈跟踪,它使调试变得更加困难。

答案 4 :(得分:1)

除了优化尾递归外,您还可以通过以下方式手动设置递归深度:

SourceSheet.Activate

答案 5 :(得分:0)

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())

这个解决方案不稳定,你永远不应该在生产中使用它。您可以阅读有关 page in github(俄语,抱歉)的一些重要限制。然而,这个解决方案非常“真实”,没有中断代码和其他类似的技巧。