6

我最近了解了一种使用装饰器来记忆递归函数的强大方法。
“嘿嘿,好巧,我们一起玩吧!”

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. “啊呸呸呸。”

经过一番搜索,我发现了一个似乎可以解决问题的 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

4 回答 4

4

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

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

所以这个故事的精神是尾调用优化和记忆不混合。

当然,对于这个特定问题,无论如何都有一种方法可以以对数步数解决问题(参见http://mitpress.mit.edu/sicp/full-text/book/book-ZH上的 SICP 练习 1.19 -11.html#%_sec_1.2.4了解更多详细信息),在这种情况下使问题变得毫无意义。但这不是这个问题的目的。

于 2013-11-01T22:48:47.453 回答
3

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

  1. 它确切地知道调用堆栈的样子;和,
  2. 如果它破坏了调用堆栈的一部分,那一点也不重要。

如果您将其全部应用到真正的尾递归函数中,那就没问题了。但是将它与另一个装饰器结合起来,#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

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

您可以再次使其复杂化,重新缝合调用堆栈以仅丢弃对装饰器的正在进行的调用tail_call_optimized,然后备忘录将再次正常工作。但是——惊喜!;-) - 那么调用堆栈仍将包含对备忘录装饰器所有级别的正在进行的调用,并且您将再次达到最大递归限制。由于 memo 函数本身不会调用结束(即,丢弃与 memo 函数调用对应的堆栈帧是正确的),因此没有简单的方法可以绕过它。

于 2013-11-02T01:04:31.973 回答
1

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

于 2013-11-01T21:13:49.410 回答
1

您遇到的是 Python 中的堆栈限制。如果你真的想走这条路,你需要做的是开始使用一种叫做Trampoline的东西。这实质上是用堆栈空间换取堆空间。

有一篇关于如何在javascript中思考这些问题的好文章和一篇更具体的Python文章。从那篇文章中,您正在寻找的是:

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

这样你就可以在不吹堆栈的情况下做事。读一读。

编辑:

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

于 2013-11-01T19:39:26.117 回答