结合memoization和尾调用优化

时间:2013-11-01 19:27:20

标签: python

我最近了解了一种将装饰器用于memoize recursive functions的强大方法。
“嘿,这很整洁,让我们玩吧!”

class memoize:
    """Speeds up a recursive function"""
    def __init__(self, function):
        self.function = function
        self.memoized = {}

    def __call__(self, *args):
        try:
            return self.memoized[args]
        except KeyError:
            self.memoized[args] = self.function(*args)
            return self.memoized[args]
#fibmemo
@memoize
def fibm(n, current=0, next=1):
    if n == 0:
        return current
    else:
        return fibm(n - 1, next, current+next)

哪个timeit显示这确实加速了算法:

fibmemo     0.000868436280412
fibnorm     0.0244713692225

“哇,这可能真的很有用!我想知道我可以推动多远?”
我发现当我开始使用高于140的输入时,我很快就遇到RuntimeError: maximum recursion depth exceeded“啊哎呀......”

经过一番搜索,我found a hack似乎可以解决问题 “这看起来也很整洁!让我们玩吧”

class TailRecurseException:
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs

def tail_call_optimized(g):
    """
    This function decorates a function with tail call optimization. It does this by throwing an exception
    if it is it's own grandparent, and catching such exceptions to fake the tail call optimization.

    This function fails if the decorated function recurses in a non-tail context.
    """
    def func(*args, **kwargs):
        f = sys._getframe()
        if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:
            raise TailRecurseException(args, kwargs)
        else:
            while 1:
                try:
                    return g(*args, **kwargs)
                except TailRecurseException, e:
                    args = e.args
                    kwargs = e.kwargs
    func.__doc__ = g.__doc__
    return func

#fibtail
@tail_call_optimized
def fibt(n, current=0, next=1):
    if n == 0:
        return current
    else:
        return fibt(n - 1, next, current+next)

好的,所以我有办法用memoize来加速我的斐波那契函数。我有办法推动递归限制。我无法弄清楚如何做到这两点。

#fibboth
@memoize
@tail_call_optimized
def fibb(n, current=0, next=1):
    if n == 0:
        return current
    else:
        return fibb(n - 1, next, current+next)
fibboth     0.00103717311766 
fibtail     0.274269805675
fibmemo     0.000844891605448
fibnorm     0.0242854266612

我已经尝试过组合装饰器,看起来像它适用于140以下的输入,但是当我超越它时,RuntimeError: maximum recursion depth exceeded发生。这几乎就像@tail_call_optimized失败了。 “什么......?”


问题:

  1. 有没有办法组合这些装饰器?如果是这样,怎么样?
  2. 为什么在组合时看起来装饰器正在为较小的输入工作?

4 个答案:

答案 0 :(得分:4)

这里有两个问题:第一个是,正如@badcook指出的那样,memoize装饰器在技术上将你的函数转换为非尾递归函数。但是,tail_call_optimized装饰器并不关心它。

第二个问题,以及它不起作用的原因是memoize装饰器每次调用fibb时都会向堆栈添加一个额外的帧。因此,它更像是自己的曾祖父母,而不是自己的祖母。您可以修复检查,但请注意,memoize装饰器将被有效绕过。

故事的士气是尾调用优化和记忆不混合。

当然,对于这个特殊的问题,有一种方法可以用对数步数来解决问题(更多细节参见http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-11.html#%_sec_1.2.4的SICP练习1.19),在这种情况下使问题变得没有实际意义。但这不是这个问题的关键所在。

答案 1 :(得分:3)

@NathanDavis在他的回答中钉了它 - 你应该接受它。 tail_call_optimized()是令人讨厌的代码,依赖于两件事:

  1. 它确切地知道调用堆栈的样子;和,
  2. 如果它破坏了部分调用堆栈,那就不重要了。
  3. 如果您将它全部应用于一个真正的尾递归函数,那么这些都很好。但是将它与另一个装饰者结合起来,#1不再是真的。您可以尝试“修复”这个(例如):

    def tail_call_optimized(g):
        def func(*args, **kwargs):
            f = sys._getframe()
            code = f.f_code
            fcount = 0
            while f:
                if f.f_code is code:
                    fcount += 1
                    if fcount > 1:
                        raise TailRecurseException(args, kwargs)
                f = f.f_back
            while 1:
                try:
                    return g(*args, **kwargs)
                except TailRecurseException, e:
                    args = e.args
                    kwargs = e.kwargs
        func.__doc__ = g.__doc__
        return func
    

    现在它会回调调用堆栈任意数量的帧以再次“找到自己”,这确实消除了递归限制异常。但是,正如内森暗示的那样,当它引发TailRecurseException时,摆脱了对memoization装饰器的正在进行的调用。最后,在调用(比如说)fibb(5000)之后,只有参数5000将出现在备忘录中。

    你可能会再次使它复杂化,重新调整调用堆栈以丢弃正在进行的tail_call_optimized装饰器调用,然后备忘录将再次正常工作。但是 - 惊喜! ;-) - 然后调用堆栈仍然包含对memo装饰器的所有级别的进行中调用,并且您再次达到最大递归限制。由于备忘录函数本身以一个调用结束(即,永远不会正确抛弃对应于备忘录函数调用的堆栈帧),所以没有简单的方法蠕虫围绕着那个。

答案 2 :(得分:1)

你要达到的是Python中的堆栈限制。如果你真的想要这样做,你需要做的是开始使用一个叫Trampoline的东西。这基本上交换了堆空间的堆栈空间。

有一篇很好的文章介绍了如何在javascript中考虑这些内容,以及针对Python更具体的内容。从那篇文章中你要找的是:

def trampoline(func):
  def decorated(*args):
    f = func(*args)
    while callable(f):
        f = f()
    return f
  return decorated

这样你就可以在不吹嘘的情况下做事。给它一个阅读。

编辑:

我还想补充一点,这是一个简单的蹦床实现。这些库有更好的版本,这就是我链接js文章的原因。你可以在其中看到一个更强大的版本,它可以处理许多类型的依赖计算,同时仍然保留尾部调用优化的想法。

答案 3 :(得分:1)

基于最简短的一瞥(现在需要跑掉),我的猜测是你的memoize装饰器破坏了尾部调用(即你的函数不再处于尾部位置),所以实际上这个函数不再是尾部调用可优化的。