Python是否优化尾部递归?


205

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

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


11
@Wessie TCO简单考虑了语言的动态或静态。例如,Lua也这样做。您只需要识别尾调用(在AST级别和字节码级别都非常简单),然后重用当前的堆栈框架而不是创建新的堆栈框架(解释器也比本机代码简单,实际上甚至更简单) 。

11
哦,一个提要:您专门谈论尾递归,但是使用首字母缩写词“ TCO”,这意味着尾部调用优化,并且适用于任何return func(...)(显式或隐式)实例,无论它是否递归。TCO是TRE的合适的超集,并且更有用(例如,它使TRE不能实现的连续传递样式可行),并且实施起来也并不困难。

1
下面是实现它的hackish方式-使用异常引发扔在执行帧走了装饰:metapython.blogspot.com.br/2010/11/...
jsbueno

2
如果您将自己限制在尾部递归上,那么我认为适当的回溯是没有用的。您有一个foo从内部到内部的呼叫,foo从一个内部到foo另一个内部的呼叫foo。。。我认为丢失此信息不会丢失任何有用的信息。
凯文(Kevin)

1
我最近了解了椰子,但还没有尝试过。看起来值得一看。据称具有尾递归优化。
阿列克谢

Answers:


214

不,而且永远不会,因为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

12
或者,如果您要像这样进行转换-只需:from operator import add; reduce(add, xrange(n + 1), csum)
乔恩·克莱门茨

38
@JonClements,在此特定示例中有效。一般情况下,转换为while循环可进行尾递归。
John La Rooy

25
+1正确答案,但这似乎是一个难以置信的笨拙的设计决定。给出原因似乎可以归结为“鉴于python的解释方式,我很难做到,因此我不喜欢它!”
2014年

12
@jwg那么...什么?您必须写一种语言才能对糟糕的设计决策发表评论?似乎不太合乎逻辑或不切实际。根据您的评论,我认为您对使用任何语言编写的任何功能(或缺少功能)都没有意见?
基本

2
@Basic不,但是您必须阅读您所评论的文章。考虑到它如何“归结为”您,您似乎并没有真正读过它。(不幸的是,您可能实际上需要阅读这两篇链接的文章,因为有些论点散布在这两篇文章中。)这几乎与语言的实现无关,而与预期的语义有关。
2016年

178

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

在Python中优化尾递归

经常有人声称尾递归不适合Pythonic的编码方式,而且人们不应该在意如何将其嵌入循环中。我不想以这种观点争论。但是有时我还是喜欢尝试或将新的想法作为尾递归函数而不是由于各种原因而使用循环(出于各种原因(着眼于想法而不是过程,同时在屏幕上同时具有二十个短函数,而不是三个“ 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

该功能可以通过以下方式使用:这是阶乘和斐波那契的尾递归版本的两个示例:

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

当然,这是该功能的唯一实际目的。

此优化只能完成一件事情:不能与评估另一个函数的尾部递归函数一起使用(这是由于可调用返回的对象都被当作没有区别的进一步递归调用来处理的事实)。由于我通常不需要这种功能,因此我对上面的代码感到非常满意。但是,为了提供一个更通用的模块,我想了一下,以便找到解决此问题的方法(请参阅下一节)。

关于这个过程的速度(但这不是真正的问题),它碰巧是相当不错的。尾递归函数甚至比使用以下代码使用更简单的表达式更快地求值:

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的方式将是捕获该异常以找到一些干净的方法解决方案,这实际上就是这里发生的事情...

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组合器。


您能否提供一个定义函数的示例(最好是类似于普通定义的函数),该函数使用您的方法根据某些条件尾部调用其他几个函数之一?另外,您的包装函数bet0可以用作类方法的装饰器吗?
阿列克谢

@Alexey我不确定我是否可以在注释内以块样式编写代码,但是您当然可以def为函数使用语法,实际上上面的最后一个示例取决于条件。在我的文章baruchel.github.io/python/2015/11/07/…中,您可以看到以“当然您可以反对没人会写这样的代码”开头的段落,在该示例中,我将使用通常的定义语法。对于您的问题的第二部分,由于我已经有一段时间没有花时间了,因此我必须多考虑一下。问候。
托马斯·巴鲁切尔'18

即使使用非TCO语言实现,也应注意函数中递归调用的位置。这是因为在递归调用之后发生的函数部分是需要存储在堆栈中的部分。因此,使函数尾部递归可以最大程度地减少每次递归调用必须存储的信息量,这为您在拥有大型递归调用堆栈时留出了更多空间。
约西亚

21

Guido的字词位于http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

我最近在我的Python历史记录博客中发布了有关Python功能特性起源的条目。关于不支持尾递归消除(TRE)的附带评论立即引发了一些评论,说明Python不能做到这一点是多么可惜,包括其他人试图“证明”可以将TRE添加到Python的最新博客条目的链接。容易。因此,让我捍卫自己的立场(这就是我不希望使用该语言的TRE)。如果您想要一个简短的答案,那简直就是不可思议。这是很长的答案:


12
这就是所谓的BDsFL的问题所在。
亚当·多纳休

6
@AdamDonahue您对委员会做出的每个决定是否感到完全满意?至少您可以从BDFL获得合理且权威的解释。
Mark Ransom 2014年

2
不,当然不是,但是它们使我更加公正。这是来自描述主义者,而不是描述主义者。具有讽刺意味的。
Adam Donahue 2014年

6

CPython不会并且可能永远不会基于Guido van Rossum关于该主题陈述来支持尾部调用优化。

我听说过有论点,因为它如何修改堆栈跟踪,这使调试更加困难。


18
@mux CPython是编程语言Python的参考实现。还有其他实现(例如PyPy,IronPython和Jython),它们实现相同的语言,但实现细节不同。这种区别在这里很有用,因为(理论上)可以创建执行TCO的替代Python实现。我什至不知道有人在想它,而且有用性会受到限制,因为依赖它的代码会在所有其他Python实现上中断。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.