Python 结合记忆化和尾部调用优化

Python 结合记忆化和尾部调用优化,python,Python,我最近了解了一种使用装饰器来实现目标的强大方法“嘿,太好了,我们来玩玩吧!” 类备忘录: “”“加速递归函数”“” 定义初始化(自身,功能): 自我功能 self.memonized={} 定义调用(self,*args): 尝试: 返回自我。已记忆[args] 除KeyError外: self.memonized[args]=self.function(*args) 返回自我。已记忆[args] #纤维备忘录 @回忆 def fibm(n,当前=0,下一个=1): 如果n==0: 回流 其他:

我最近了解了一种使用装饰器来实现目标的强大方法
“嘿,太好了,我们来玩玩吧!”

类备忘录:
“”“加速递归函数”“”
定义初始化(自身,功能):
自我功能
self.memonized={}
定义调用(self,*args):
尝试:
返回自我。已记忆[args]
除KeyError外:
self.memonized[args]=self.function(*args)
返回自我。已记忆[args]
#纤维备忘录
@回忆
def fibm(n,当前=0,下一个=1):
如果n==0:
回流
其他:
返回fibm(n-1,下一个,当前+下一个)
这表明这确实加快了算法的速度:

“哇,那真的很有用!我想知道我能把它推多远?”
我发现当我开始使用高于140的输入时,我很快就遇到了
运行时错误:超过了最大递归深度。“啊,见鬼……”

经过一点搜索,我发现这似乎解决了问题。
“这个看起来也很漂亮!我们来玩玩吧”

类尾递归异常:
定义初始化(self、args、kwargs):
self.args=args
self.kwargs=kwargs
def尾部调用优化(g):
"""
此函数使用尾部调用优化来装饰函数。它通过引发异常来完成此操作
如果是它自己的祖父母,捕获这样的异常以伪造尾部调用优化。
如果修饰函数在非尾部上下文中递归,则此函数失败。
"""
def func(*args,**kwargs):
f=sys.\u getframe()
如果f.f_back和f.f_back.f_back和f.f_back.f_back.f_code==f.f_code:
升起尾递归异常(args、kwargs)
其他:
而1:
尝试:
返回g(*args,**kwargs)
除TailRecurseException外,e:
args=e.args
kwargs=e.kwargs
职能文件__
返回函数
#纤尾
@尾部呼叫优化
def fibt(n,当前=0,下一个=1):
如果n==0:
回流
其他:
返回fibt(n-1,下一个,当前+下一个)
好的,我有一种方法可以使用memoize来加速斐波那契函数。我有办法突破递归极限。我不知道如何做到这两个

#两者皆有
@回忆
@尾部呼叫优化
def fibb(n,当前=0,下一个=1):
如果n==0:
回流
其他:
返回fibb(n-1,下一个,当前+下一个)
0.00103717311766
fibtail 0.274269805675
fibmemo 0.000844891605448
fibnorm 0.0242854266612
我尝试过组合decorator,看起来它适用于140以下的输入,但当我超过这个值时,
RuntimeError:maximum recursion depth excelled
。这几乎就像优化的
@tail\u call\u
失败了一样。 “什么……”


问题:

  • 有没有办法把这些装饰师结合起来?如果是,怎么做
  • 为什么当组合在一起时,装饰器似乎在为较小的输入工作

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

    有一篇关于如何在中思考这些问题的好文章,还有一篇更具体的文章。从那篇文章中,您要查找的是:

    def trampoline(func):
      def decorated(*args):
        f = func(*args)
        while callable(f):
            f = f()
        return f
      return decorated
    
    这样你就可以在不吹烟囱的情况下做事了。读一读

    编辑:


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

    根据最简短的一眼(现在需要运行),我猜您的memoize装饰程序已经破坏了尾部调用(即,您的功能不再处于尾部位置),因此实际上函数不再是尾部调用优化的。

    这里有两个问题:第一个问题,正如@badcook指出的,memoize装饰器从技术上将函数转换为非尾部递归函数。然而,尾部调用优化装饰器并不关心这一点

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

    因此,这个故事的寓意是尾部调用优化和备忘录化不能混为一谈


    当然,对于这个特定的问题,无论如何都有一种方法可以以对数的步骤来解决这个问题(更多细节,请参见SICP练习1.19),这使得这个问题在本例中非常没有意义。但这不是这个问题的内容。

    @NathanDavis在他的答案中明确了这一点-你应该接受它。
    tail\u call\u optimized()
    是令人讨厌的代码,依赖于两件事:

  • 它确切地知道调用堆栈的外观;并且
  • 如果它破坏了调用堆栈的一部分,这一点都不重要
  • 如果您单独将其应用于真正的尾部递归函数,这些都是可以的。但是将其与另一个decorator结合使用,则#1不再是真的。您可以尝试“修复”它,如下所示(例如):

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

    你可能会再次把事情复杂化
    def trampoline(func):
      def decorated(*args):
        f = func(*args)
        while callable(f):
            f = f()
        return f
      return decorated
    
    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