【问题标题】:Does Python optimize tail recursion?Python 是否优化尾递归?
【发布时间】:2012-11-15 12:44:15
【问题描述】:

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

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

【问题讨论】:

  • @Wessie TCO 简单地考虑了语言的动态或静态。例如,Lua 也是这样做的。您只需要识别尾调用(在 AST 级别和字节码级别都非常简单),然后重新使用当前堆栈帧而不是创建新堆栈帧(也很简单,实际上在解释器中比在本机代码中更简单) .
  • 哦,吹毛求疵:你专门谈论尾递归,但使用首字母缩略词“TCO”,这意味着尾调用优化并适用于任何 return func(...) 的实例(显式或隐式),无论它是否递归。 TCO 是 TRE 的适当超集,并且更有用(例如,它使连续传递样式可行,而 TRE 不能),并且实施起来并不困难。
  • 这里有一个 hackish 的方式来实现它 - 一个装饰器使用异常引发来抛出执行帧:metapython.blogspot.com.br/2010/11/…
  • 如果你限制自己使用尾递归,我不认为适当的回溯是超级有用的。您可以从内部调用foo 从内部调用foo 从内部调用foo 从内部调用foo...我认为丢失此信息不会丢失任何有用的信息。跨度>
  • 我最近了解了Coconut,但还没有尝试过。看起来值得一看。据称有尾递归优化。

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


【解决方案1】:

CPython 不支持也可能永远不会支持基于 Guido van Rossum's 语句的尾调用优化。

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

【讨论】:

  • @mux CPython 是编程语言 Python 的参考实现。还有其他实现(例如 PyPy、IronPython 和 Jython),它们实现了相同的语言,但实现细节不同。这种区别在这里很有用,因为(理论上)可以创建一个替代的 Python 实现来实现 TCO。不过,我不知道任何人都在考虑它,并且有用性会受到限制,因为依赖它的代码会破坏所有其他 Python 实现。
【解决方案2】:

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

Tail Recursion Elimination (2009-04-22)

Final Words on Tail Calls (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

【讨论】:

  • 或者,如果您要这样改造它 - 只需:from operator import add; reduce(add, xrange(n + 1), csum) ?
  • @JonClements,在这个特定的例子中有效。在一般情况下,转换为 while 循环适用于尾递归。
  • +1 作为正确答案,但这似乎是一个令人难以置信的愚蠢的设计决定。 reasons given 似乎可以归结为“鉴于 python 的解释方式,这很难做到,而且我不喜欢它,所以在那里!”
  • @jwg 那么...什么?您必须先编写一种语言,然后才能评论糟糕的设计决策?看起来几乎没有逻辑或实用性。我从您的评论中假设您对任何语言的任何功能(或缺乏)没有意见?
  • @Basic 不,但您必须阅读您正在评论的文章。考虑到它对您的“归结”,您似乎没有真正阅读它,这似乎非常强烈。 (不幸的是,您实际上可能需要阅读两篇链接的文章,因为两者都有一些争论。)它几乎与语言的实现无关,而是与预期的语义有关。
【解决方案3】:

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

我最近在我的 Python 历史博客中发布了一篇关于 Python 的功能特性。关于不支持尾巴的旁白 递归消除(TRE)立即引发了几个关于 可惜的是 Python 没有这样做,包括指向 最近其他人试图“证明”可以添加 TRE 的博客条目 轻松到 Python。所以让我捍卫我的立场(也就是说,我不 想要语言中的 TRE)。如果您想要一个简短的答案,那很简单 非pythonic。这是长答案:

【讨论】:

  • 这就是所谓的 BDsFL 的问题。
  • @AdamDonahue 你对委员会的每一个决定都非常满意吗?至少你从 BDFL 那里得到了一个合理而权威的解释。
  • 不,当然不是,但我觉得他们更公平。这是来自规范主义者,而不是描述主义者。具有讽刺意味。
【解决方案4】:

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

在 Python 中优化尾递归

经常有人声称尾递归不适合 Pythonic 的编码方式,而且人们不应该关心如何将它嵌入到循环中。我不想争论 这种观点;但有时我喜欢尝试或实施新想法 由于各种原因,作为尾递归函数而不是循环(专注于 想法而不是过程,在我的屏幕上同时有二十个简短的功能 时间而不是只有三个“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

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

>>> 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 不喜欢尾递归调用,则异常 当确实发生尾递归调用时应该引发,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 函数嵌入到另一个函数中,该函数将其评估为循环。

以我的拙见,这个小函数中最有趣的特性是该函数不依赖于一些肮脏的编程技巧,而是仅仅依赖于 lambda 演算:当插入另一个函数时,该函数的行为会更改为另一个lambda 函数,看起来很像 Y 组合器。

【讨论】:

  • 您能否提供一个定义函数的示例(最好以类似于正常定义的方式),该函数根据某些条件使用您的方法尾调用其他几个函数之一?另外,你的包装函数bet0 可以用作类方法的装饰器吗?
  • @Alexey 我不确定我是否可以在注释中以块样式编写代码,但您当然可以为您的函数使用def 语法,实际上上面的最后一个示例依赖于一个条件。在我的帖子baruchel.github.io/python/2015/11/07/… 中,您可以看到以“当然,您可以反对没有人会编写这样的代码”开头的段落,其中我举了一个具有通常定义语法的示例。对于你问题的第二部分,我必须再考虑一下,因为我已经有一段时间没有花时间在这上面了。问候。
  • 您应该关心递归调用在您的函数中发生的位置,即使您使用的是非 TCO 语言实现。这是因为在递归调用之后发生的函数部分是需要存储在堆栈上的部分。因此,使您的函数尾递归可以最大限度地减少每次递归调用必须存储的信息量,从而为您提供更多空间来拥有大型递归调用堆栈(如果需要)。
【解决方案5】:

尝试实验性的macropy TCO 实现以了解大小。

【讨论】:

    【解决方案6】:

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

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

    【讨论】:

    【解决方案7】:

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

    【讨论】:

      猜你喜欢
      • 2011-09-04
      • 1970-01-01
      • 1970-01-01
      • 2011-01-25
      • 2014-06-23
      • 1970-01-01
      • 2017-04-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多