【问题标题】:Is it advisable to write recursive functions in Python是否建议在 Python 中编写递归函数
【发布时间】:2014-08-13 22:47:06
【问题描述】:

作为实验的一部分,我在 python 中编写了一个 verilog(基本上是逻辑门及其连接描述)模拟器。

我遇到了堆栈限制的问题,所以我做了一些阅读,发现 Python 没有“尾调用优化”功能(即在递归进行时动态删除堆栈条目)

这方面我主要有两个问题:

1) 如果我将堆栈限制提高到sys.setrecursionlimit(15000),它会在时间方面影响性能吗(内存——我不在乎)?

2) 假设我可以在没有堆栈跟踪的情况下生活,有什么办法可以绕过这个限制。
我问这个是因为 Verilog 主要处理可以使用递归函数以优雅的方式实现的状态机。

另外,如果我可以添加,在递归函数调用的情况下,如果存在错误,我会更多地依赖导致此错误的输入而不是堆栈跟踪。

我是 Python 新手,所以专家们可能会争辩说 Python 堆栈跟踪对于调试递归函数调用非常有用……如果是这样的话,我会非常乐意学习如何做到这一点。

最后,是否建议用 Python 编写递归函数,还是应该转向其他语言?

如果有任何解决方法可以让我继续使用 python 进行递归函数,我想知道是否有任何性能影响(不过我可以进行分析)。

【问题讨论】:

  • 我不知道它在理论上如何影响性能,但我从来没有遇到过重大问题,并且总是将 sys.setrecursionlimit 设置为最大可能值,因为我遇到了大型类/函数的问题默认的最大递归深度。也就是说,正如下面的答案所解释的那样,除了一长串的递归调用之外,大多数时候有一种更快的方法来做你正在做的任何事情。
  • 如果可以做尾调用优化,那么就不需要栈来模拟递归了。将其转换为仅使用常规无堆栈循环将是微不足道的。

标签: python recursion


【解决方案1】:

2) 假设我可以在没有堆栈跟踪的情况下生活,有什么办法可以绕过这个限制。 我问这个是因为 Verilog 主要处理可以使用递归函数以优雅方式实现的状态机。

有一种方法可以在不过多更改现有逻辑的情况下避免尾调用,只需重写尾调用以返回一个 thunk 并使用 trampoline 来调用该 thunk。如果您需要在转换之间传递复杂的状态,您可以使用continuation passing style 来传递它们。这种编写代码的方式非常适合编写状态机。

一个例子可能更清楚,假设您从 fizzbuzz 状态机的递归实现开始,该状态机使用尾调用将控制权传递给下一个转换:

def start():
    return increment(0)

def fizz(n):
    print 'fizz'
    return increment(n)

def buzz(n):
    print 'buzz'
    return increment(n)

def fizzbuzz(n):
    print 'fizzbuzz'
    return increment(n)

def increment(n):
    n = n + 1
    if n > 100:
        return terminate()
    elif n % 3 == 0 and n % 5 == 0: 
        return fizzbuzz(n)
    elif n % 3 == 0: 
        return fizz(n)
    elif n % 5 == 0:
        return buzz(n)
    else:
        print n
        return increment(n)

def terminate():
    raise StopIteration

try:
    start()
except StopIteration:
    pass

为避免尾部调用,您只需将所有尾部调用包装在 lambda(或者 functools.partial)中并添加一个蹦床:

def start():
    return lambda: increment(0)

def fizz(n):
    print 'fizz'
    return lambda: increment(n)

def buzz(n):
    print 'buzz'
    return lambda: increment(n)

def fizzbuzz(n):
    print 'fizzbuzz'
    return lambda: increment(n)

def increment(n):
    n = n + 1
    if n > 2000:
        # strictly speaking, transitions that takes no arguments
        # like terminate don't need to be wrapped in lambda
        # this is added here only for symmetry with others
        return lambda: terminate()
    elif n % 3 == 0 and n % 5 == 0: 
        return lambda: fizzbuzz(n)
    elif n % 3 == 0: 
        return lambda: fizz(n)
    elif n % 5 == 0:
        return lambda: buzz(n)
    else:
        print n
        return lambda: increment(n)

def terminate():
    raise StopIteration

def trampoline(func): 
    try:
        while True:
            func = func()
    except StopIteration:
        pass

trampoline(lambda: start())

现在您可以在不达到递归限制的情况下拥有更多的 fizzbuzz。

【讨论】:

  • 非常感谢您的指点!!这解决了我的递归函数问题,但无论如何我已经转向解决我的问题的非递归方式......递归解决方案很慢
  • 感谢您向我介绍了一种新的(对我而言)编程方式!我不确定这种模式是否已经深深印在我的脑海中,但是在某些时候尝试一下会很有趣。使用 thunk 和 trampolines 是一个非常巧妙的想法,并且可以(显然)用于实现一些相当有趣的算法。你是先写后转换?
【解决方案2】:

很大程度上取决于您尝试实施的递归解决方案的具体性质。让我举一个具体的例子。假设您想要列表中所有值的总和。您可以通过将第一个值添加到列表其余部分的总和来设置递归 - 递归应该是显而易见的。然而,递归子问题仅比原始问题小 1,因此递归堆栈将增长到与列表中的项目数一样大。对于大型列表,这将是一个问题。另一种递归是注意所有值的总和是列表前半部分的总和加上列表后半部分的总和。同样,递归应该是显而易见的,终止条件是当您到达长度为 1 的子列表时。但是,对于这个版本,堆栈只会增长为列表大小的 log2,并且您可以处理大量列表而不会出现堆栈问题。并非所有问题都可以分解为一半大小的子问题,但如果可以,这是避免堆栈溢出情况的好方法。

如果您的递归解决方案是尾递归,您可以轻松转换为循环而不是递归调用。

如果您没有尾递归,另一种可能性是使用循环实现事物并将中间状态显式存储在显式堆栈中。

【讨论】:

  • 所以你在建议一种 fork-join 方法......你的建议意味着我必须重组我的程序(显着)......我提出这个问题的主要动机是看看是否我可以选择使用某种机制,即使它需要 10k+ PS,我也可以以 1000 的堆栈限制(比如说)运行我的程序:我实际上正在编辑我的代码以有几个独立的列表来并行解决。跨度>
【解决方案3】:

Does Python optimize tail recursion?

Guido Van Rossum 说使用大量递归是“完全不符合 Python 的”:http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

但无论如何,许多人都试图破坏自己的支持。例如。 http://tomforb.es/adding-tail-call-optimization-to-python 。或者只是谷歌“python尾调用”

【讨论】:

  • 这是一个很好的实现。不幸的是,我看到的谷歌结果相当糟糕,甚至不支持相互递归。
【解决方案4】:

注意:此答案仅限于您最重要的问题,即“是否建议在 Python 中编写递归函数?”。

简短的回答是否定的,这并不完全是“可取的”。如果没有尾调用优化,Python 中的递归会变得非常缓慢,因为函数调用在内存和处理器时间上都非常密集。只要有可能,最好迭代地重写你的代码。

【讨论】:

    【解决方案5】:

    特别解决您标记为 1) 的问题,更改递归限制是危险的,因为它可能会导致底层 C 堆栈溢出。另请参阅此问题:What is the maximum recursion depth in Python, and how to increase it?

    【讨论】:

      【解决方案6】:

      我使用sys.setrecursionlimit 将递归限制设置为其最大可能值,因为我遇到了大型类/函数达到默认最大递归深度的问题。为递归限制设置一个较大的值不应影响脚本的性能,即,如果它在递归上限和下限下完成,它将花费相同的时间来完成。唯一的区别是,如果你有一个低递归限制,它会阻止你做愚蠢的事情(比如运行一个无限递归循环)。使用一个上限,而不是达到上限,一个使用递归过多的极其低效的脚本将永远运行(或者直到它耗尽内存,具体取决于任务)。

      正如其他答案更详细地解释的那样,除了一连串的递归调用之外,大多数情况下,有一种更快的方法来执行您正在执行的任何操作。

      【讨论】:

        【解决方案7】:

        我看到装饰器试图在 python 中实现尾递归,所以我自己尝试了一下。这是一个允许相互递归的尾递归优化的纯 Python(无 sys._getframe)实现。

        class TailRecurseException(Exception):
            def __init__(self, func, args, kwargs):
                self.func = func
                self.args = args
                self.kwargs = kwargs
        
        def tail_recursive(g, rec=[]):
            def func(*args, **kwargs):
                if g in rec:
                    raise TailRecurseException(g, args, kwargs)
                rec.append( g )
                while True:
                   try:
                       r = g(*args, **kwargs)
                       rec.remove( g )
                       return r
                   except TailRecurseException, e:
                       if e.func==g:
                           args = e.args
                           kwargs = e.kwargs
                       else:
                           rec.remove( g )
                           raise e
            return func
        
        @tail_recursive
        def g(n):
            if n==0:
                return 0
            else:
                return f(n-1)
        
        @tail_recursive
        def f(n):
            if n == 0:
                return 0
            else:
                return g(n-1)
        
        print f(100000)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-23
          • 1970-01-01
          • 2015-07-03
          • 1970-01-01
          • 2014-08-15
          相关资源
          最近更新 更多