简体   繁体   English

是否可以在Python中编写递归函数

[英]Is it advisable to write recursive functions in Python

I have written a verilog (logic gates and their connectivity description basically) simulator in python as a part of an experiment. 我已经在python中编写了一个verilog(逻辑门及其连接描述)模拟器作为实验的一部分。

I faced an issue with the stack limit so I did some reading and found that Python does not have a "tail call optimization" feature (ie removing stack entries dynamically as recursion proceeds) 我遇到了堆栈限制的问题,所以我做了一些阅读,发现Python没有“尾调用优化”功能(即随着递归的进行动态删除堆栈条目)

I mainly have two questions in this regard: 我在这方面主要有两个问题:

1) If I bump up the stack limit to sys.setrecursionlimit(15000) does it impact performance in terms of time (memory -- I do not care)? 1) 如果我将堆栈限制提升到sys.setrecursionlimit(15000)它是否会影响性能(内存 - 我不在乎)?

2) Is there any way I can circumvent this limitation assuming that I can live without a stack-trace. 2) 我有什么方法可以绕过这个限制,假设我可以没有堆栈跟踪。
I ask this because Verilog mainly deals with state-machines which can be implemented in an elegant way using recursive functions. 我问这个是因为Verilog主要处理可以使用递归函数以优雅方式实现的状态机。

Also, if I may add, in case of recursive function calls, if there is a bug, I rely more on the input which is causing this bug rather than the stack trace. 另外,如果我可以添加,在递归函数调用的情况下,如果有错误,我更依赖于导致此错误的输入而不是堆栈跟踪。

I am new to Python, so maybe experts might argue that the Python stack trace is quite useful to debug recursive function calls...if that is the case, I would be more than happy to learn how to do that. 我是Python新手,所以也许专家可能认为Python堆栈跟踪对调试递归函数调用非常有用......如果是这种情况,我会非常乐意学习如何做到这一点。

Lastly, is it advisable to write recursive functions in Python or should I be moving to other languages? 最后,建议在Python中编写递归函数还是应该转移到其他语言?

If there any work-around such that I can continue using python for recursive functions, I would like to know if there any performance impact (I can do profiling though). 如果有任何解决方法,我可以继续使用python进行递归函数,我想知道是否有任何性能影响(我可以做分析)。

2) Is there any way I can circumvent this limitation assuming that I can live without a stack-trace. 2)我有什么方法可以绕过这个限制,假设我可以没有堆栈跟踪。 I ask this because Verilog mainly deals with state-machines which can be implemented in an elegant way using recursive functions. 我问这个是因为Verilog主要处理可以使用递归函数以优雅方式实现的状态机。

There is a way to avoid tail calls without changing your existing logic too much, simply rewrite your tail calls to return a thunk and use a trampoline to call that thunk. 有一种方法可以避免尾调用而不会过多地更改现有逻辑,只需重写尾调用以返回thunk并使用trampoline来调用thunk。 If you need to pass in complex state between transition, you can use continuation passing style to pass them around. 如果需要在转换之间传递复杂状态,可以使用连续传递样式来传递它们。 This style of writing code is very well suited for writing a state machine. 这种编写代码的方式非常适合编写状态机。

An example is perhaps clearer, suppose that you start with this recursive implementation of a fizzbuzz state machine that uses tail calls to pass control to the next transition: 一个例子可能更清楚,假设您从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

To avoid the tail calls, you simply wrap all the tail calls in lambda (or alternatively, functools.partial) and add a trampoline: 要避免尾调用,只需将所有尾调用包装在lambda(或者functools.partial)中并添加一个trampoline:

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

Now you can have lots more fizzbuzz without hitting the recursion limit. 现在你可以有更多的fizzbuzz而不会达到递归限制。

A lot depends on the specific nature of the recursive solution you're trying to implement. 很大程度上取决于您尝试实现的递归解决方案的特定性质。 Let me give a concrete example. 让我举一个具体的例子。 Suppose you want the sum of all values in a list. 假设您想要列表中所有值的总和。 You can set the recursion up by adding the first value to the sum of the remainder of the list - the recursion should be obvious. 您可以通过将第一个值添加到列表其余部分的总和来设置递归 - 递归应该是显而易见的。 However, the recursive subproblem is only 1 smaller than the original problem, so the recursive stack will grow to be as big as the number of items in the list. 但是,递归子问题仅比原始问题小1,因此递归堆栈将增长到与列表中的项目数一样大。 For large lists this will be a problem. 对于大型列表,这将是一个问题。 An alternate recursion is to note that the sum of all values is the sum of the first half of the list plus the sum of the second half of the list. 另一种递归是要注意所有值的总和是列表的前半部分加上列表后半部分的总和。 Again, the recursion should be obvious and the terminating condition is when you get down to sublists of length 1. However, for this version the stack will only grow as log 2 of the size of the list, and you can handle immense lists without stack problems. 同样,递归应该是显而易见的,并且终止条件是当您到达长度为1的子列表时。但是,对于此版本,堆栈将仅增长为列表大小的log 2 ,并且您可以处理没有堆栈的巨大列表问题。 Not all problems can be factored into subproblems which are half the size, but when you can this is a good way to avoid stack overflow situations. 并非所有问题都可以考虑到一半大小的子问题,但是当你可以这样做时,这是避免堆栈溢出情况的好方法。

If your recursive solution is a tail recursion, you can easily be converted into a loop rather than a recursive call. 如果递归解决方案是尾递归,则可以轻松转换为循环而不是递归调用。

Another possibility if you don't have tail recursion is to implement things with a loop and explicitly store your intermediate state on an explicit stack. 如果没有尾递归,另一种可能性是使用循环实现事物并在显式堆栈上显式存储中间状态。

See Does Python optimize tail recursion? 请参阅Python优化尾递归?

Guido Van Rossum says that using a lot of recursion is "simply unpythonic" : http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html Guido Van Rossum表示,使用大量递归是“简单的非语音”: http ://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

But many people have tried to hack up their own support anyway. 但无论如何,许多人都试图破解自己的支持。 Eg http://tomforb.es/adding-tail-call-optimization-to-python . 例如http://tomforb.es/adding-tail-call-optimization-to-python Or just google "python tail call" 或者只是google“python tail call”

Note: This answer is limited to your topmost question, ie "Is it advisable to write recursive functions in Python?". 注意:这个答案仅限于您最关心的问题,即“在Python中编写递归函数是否明智?”。

The short answer is no, it's not exactly "advisable". 简短的回答是否定的,这不是“明智的”。 Without tail-call optimization, recursion can get painfully slow in Python given how intensive function calls are on both memory and processor time. 在没有尾调用优化的情况下,考虑到内存和处理器时间上的强大函数调用,递归在Python中会变得非常缓慢。 Whenever possible, it's best to rewrite your code iteratively. 只要有可能,最好迭代地重写代码。

Addressing specifically your question marked 1), changing the recursion limit is dangerous in that it may allow an overflow of the underlying C stack. 特别针对标记为1)的问题,更改递归限制是危险的,因为它可能允许底层C堆栈溢出。 See also this question: What is the maximum recursion depth in Python, and how to increase it? 另请参阅此问题: Python中的最大递归深度是多少,以及如何增加它?

I use sys.setrecursionlimit to set the recursion limit to its maximum possible value because I have had issues with large classes/functions hitting the default maximum recursion depth. 我使用sys.setrecursionlimit将递归限制设置为其最大可能值,因为我遇到了大类/函数达到默认最大递归深度的问题。 Setting a large value for the recursion limit should not affect the performance of your script, ie it will take the same amount of time to complete if it completes under both a high and a low recursion limit. 为递归限制设置较大的值不应该影响脚本的性能,即如果它在高递增和低递归限制下完成,则需要相同的时间才能完成。 The only difference is that if you have a low recursion limit, it prevents you from doing stupid things (like running an infinitely recursive loop). 唯一的区别是,如果你有一个低递归限制,它会阻止你做愚蠢的事情(比如运行无限递归循环)。 With a high limit, rather than hit the limit, a horribly inefficient script that uses recursion too much will just run forever (or until it runs out of memory depending on the task). 有了一个上限,而不是达到极限,一个使用递归过多的非常低效的脚本将永远运行(或直到它耗尽内存,具体取决于任务)。

As the other answers explain in more detail, most of the time there is a faster way to do whatever it is that you are doing other than a long series of recursive calls. 正如其他答案更详细地解释的那样,大多数情况下,除了一系列递归调用之外,还有一种更快的方式来做你正在做的事情。

I have seen decorators trying to implement tail-recursion in python, so I took a stab at it myself. 我看到装饰器试图在python中实现尾递归,所以我自己就把它刺了一下。 Here is a pure python (no sys._getframe) implementation of tail-recursion optimization that allows for mutual recursion. 这是一个纯粹的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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM