Python中高效的记忆化技术

20

我有一些任务要解决,目前最重要的部分是尽可能地使脚本时间效率更高。我试图优化的元素之一是在一个函数中的备忘录。

所以我的问题是:以下3-4种方法中哪种是Python实现备忘录最有效/最快的方法?

我只提供了示例代码 - 如果其中一种方法更有效,但不适用于我提到的情况,请分享您所知道的内容。

解决方案1-使用来自外部作用域的可变变量

这个解决方案通常被展示为备忘录的示例,但我不确定它有多有效。我听说使用全局变量(在这种情况下是来自外部而不是全局作用域的变量)效率较低。

def main():
    memo = {}
    def power_div(n):
        try:
            return memo[n]
        except (KeyError):
            memo[n] = (n ** 2) % 4  # example expression, should not matter
            return memo[n]
    # extensive usage of power_div() here

解决方案2 - 使用默认的可变参数

我在某个地方发现过使用默认的可变参数将变量从外部作用域传递到内部函数,当Python首先在局部作用域中搜索变量,然后在全局作用域中搜索变量,跳过非局部作用域(在本例中是函数内的作用域)时,这种方式被使用过。因为默认参数仅在定义函数时初始化,并且仅在内部函数中可访问,所以可能更有效率?

def main():
    def power_div(n, memo={}):
        try:
            return memo[n]
        except (KeyError):
            memo[n] = (n ** 2) % 4  # example expression, should not matter
            return memo[n]
    # extensive usage of power_div() here

或者下面这个版本(实际上是解决方案1和2的组合)更有效呢?

def main():
    memo = {}
    def power_div(n, memo=memo):
        try:
            return memo[n]
        except (KeyError):
            memo[n] = (n ** 2) % 4  # example expression, should not matter
            return memo[n]
    # extensive usage of power_div() here

解决方案3 - 函数属性

这是Python中另一种常见的记忆化例子 - 记忆化对象作为函数本身的一个属性被存储。

def main():
    def power_div(n):
        memo = power_div.memo
        try:
            return memo[n]
        except (KeyError):
            memo[n] = (n ** 2) % 4  # example expression, should not matter
            return memo[n]
    # extensive usage of power_div() here

总结

我对上述四种记忆化解决方案非常感兴趣。重要的是,使用记忆化的函数必须在另一个函数中。

我知道还有其他记忆化解决方案(如Memoize装饰器),但很难相信这比上面列出的更有效。如果我错了,请纠正我。

提前致谢。


和大多数“哪个更快”的问题一样,最终答案是“试一下就知道了”。timeit 模块提供了一种非常好的测试方式来测试这类事情。 - Amber
你是否对现有代码进行了分析,并发现记忆化是瓶颈?如果没有,为什么要专注于优化它? - Amber
1
@Amber:情况是1)我没有太多可以优化的现有代码,因此我正在尝试改进所有我能改进的地方;2)这个问题更涉及提到的情况的效率以及为什么一个比另一个更好,它更为一般。我不想使用 timeit,因为1)我可能会错过其他更有效的解决方案;2)我的结果可能会因我使用备忘录的方式而出现偏差。我正在尝试找到使用备忘录的最快方法来学习并让人们知道,而不一定是修复这一段代码(这样的问题将过于局限)。 - Tadeck
我的直觉是使用字典对象的get()方法比捕获KeyError更快。但也有可能加速只会影响“缓存未命中”的分支,这种情况下可能不值得。不过最好还是对两种方式进行计时比较一下。 - Daniel Pryden
@DanielPryden:我一直在考虑使用get(),但是由于如果没有找到键就需要计算某些东西,所以看起来会像这样:memo.get(n, (n ** 2) % 4)。在这种情况下,这样做没有太多意义,因为(n ** 2) % 4将在每次调用函数时执行(因此记忆化将无效)。 - Tadeck
显示剩余7条评论
2个回答

16

变量访问的不同方式已经在以下链接进行了时间和比较: http://code.activestate.com/recipes/577834-compare-speeds-of-different-kinds-of-access-to-var 以下是简要总结:局部访问优于非局部(嵌套作用域),其次是全局访问(模块作用域),最后是内置函数的访问。

您的解决方案#2(使用局部访问)应该胜出。解决方案#3具有缓慢的点查找(需要字典查找)。解决方案#1使用非局部(嵌套作用域)访问,它使用单元格变量(比字典查找快但比局部变量慢)。

还要注意, KeyError 异常类是全局查找,可以通过本地化来加速。您可以完全替换try / except并使用 memo.get(n,sentinel)而不是。甚至通过使用绑定方法,这也可以加速。当然,您最容易的速度提升可能只是尝试一下 pypy :-)

简而言之,有很多方法可以调整此代码。只要确保它值得。


非常感谢 :) 你认为在使用memo=memo(其中memo在非本地作用域中)和memo={}(因此没有涉及非本地作用域)之间是否存在性能差异? - Tadeck
1
@Tadeck 完全没有区别。两种方法最终都会得到一个直接指向字典实例的局部变量。 - Raymond Hettinger

3

为了帮助那些在寻找在Python中进行记忆化的方法时偶然发现这个问题的人,我建议使用fastcache

它适用于Python 2和3,比上面描述的任何方法都要快,并提供了限制缓存大小的选项,以避免意外变得过大:

from fastcache import clru_cache

@clru_cache(maxsize=128, typed=False)
def foo(cat_1, cat_2, cat_3):
    return cat_1 + cat_2 + cat_3

安装 fastcache 很简单,使用 pip

pip install fastcache

或者conda

conda install fastcache

3
在Python 3中,您可以使用本机的functools.lru_cache。根据我的实验,它甚至比fastcache版本更快。 - alyaxey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接