什么是 memoization 以及如何在 Python 中使用它?

Posted

技术标签:

【中文标题】什么是 memoization 以及如何在 Python 中使用它?【英文标题】:What is memoization and how can I use it in Python? 【发布时间】:2010-12-31 14:55:08 【问题描述】:

我刚开始使用 Python,我不知道 memoization 是什么以及如何使用它。另外,我可以举一个简化的例子吗?

【问题讨论】:

当相关***文章的第二句包含短语“在一个通用的自顶向下解析算法[2][3] 中的相互递归下降解析[1] 时,它可以容纳歧义和左递归”多项式时间和空间,”我认为问 SO 发生了什么是完全合适的。 @Clueless:该短语前面是“记忆也已在其他情况下使用(并且用于速度增益以外的目的),例如 in”。所以这只是一个例子列表(不需要理解);这不是记忆化解释的一部分。 @StefanGruenwald 该链接已失效。你能找到更新吗? pdf 文件的新链接,因为 pycogsci.info 已关闭:people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf @Clueless,这篇文章实际上是说“简单在一个通用的自顶向下解析算法[2][3]中的相互递归下降解析[1],它适应了歧义和多项式时间和空间的左递归”。您错过了简单,这显然使该示例更加清晰:)。 【参考方案1】:

记忆化实际上是指根据方法输入记住(“记忆化”→“备忘录”→要记住)方法调用的结果,然后返回记住的结果,而不是再次计算结果。您可以将其视为方法结果的缓存。有关详细信息,请参阅第 387 页以了解 Introduction To Algorithms (3e), Cormen 等人中的定义。

在 Python 中使用 memoization 计算阶乘的简单示例如下所示:

factorial_memo = 
def factorial(k):
    if k < 2: return 1
    if k not in factorial_memo:
        factorial_memo[k] = k * factorial(k-1)
    return factorial_memo[k]

你可以再复杂一点,把memoization过程封装成一个类:

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = 
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        #Warning: You may wish to do a deepcopy here if returning objects
        return self.memo[args]

然后:

def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

factorial = Memoize(factorial)

在 Python 2.4 中添加了一个名为“decorators”的功能,现在您只需编写以下代码即可完成相同的操作:

@Memoize
def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

Python Decorator Library 有一个类似的装饰器 memoized,它比此处显示的 Memoize 类更健壮。

【讨论】:

感谢您的建议。 Memoize 类是一个优雅的解决方案,可以轻松应用于现有代码,无需大量重构。 Memoize 类解决方案有问题,它不会像factorial_memo 一样工作,因为def factorial 中的factorial 仍然调用旧的 unmemoize factorial 对了,你也可以写if k not in factorial_memo:,读起来比if not k in factorial_memo:好。 作为装饰师真的应该这样做。 @durden2.0 我知道这是一条旧评论,但 args 是一个元组。 def some_function(*args) 使 args 成为一个元组。【参考方案2】:

functools.cache装饰者:

Python 3.9 发布了一个新函数functools.cache。它将使用一组特定参数调用的函数的结果缓存在内存中,这就是记忆化。使用方便:

import functools

@functools.cache
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

这个没有装饰器的功能很慢,试试fib(36),你要等十秒左右。

添加cache 装饰器可确保如果最近针对特定值调用了该函数,它不会重新计算该值,而是使用缓存的先前结果。在这种情况下,它会带来巨大的速度提升,同时代码不会因为缓存的细节而杂乱无章。

functools.lru_cache装饰者:

如果您需要支持旧版本的 Python,functools.lru_cache 适用于 Python 3.2+。默认情况下,它只缓存最近使用的 128 个调用,但您可以将 maxsize 设置为 None 以指示缓存永不过期:

@functools.lru_cache(maxsize=None)
def fib(num):
    # etc

【讨论】:

试过 fib(1000),得到 RecursionError: 比较中超出最大递归深度 @Andyk 默认 Py3 递归限制为 1000。第一次调用 fib 时,它需要递归到基本情况才能发生记忆。所以,你的行为是意料之中的。 如果我没记错的话,它只会缓存直到进程没有被杀死,对吧?还是不管进程是否被杀死都会缓存?例如,假设我重新启动系统 - 缓存的结果是否仍会被缓存? @Kristada673 是的,它存储在进程的内存中,而不是磁盘上。 请注意,这甚至可以加快函数的第一次运行,因为它是一个递归函数并且正在缓存自己的中间结果。可能很好地说明一个非递归函数,它本质上很慢,让像我这样的傻瓜更清楚。 :D【参考方案3】:

其他答案涵盖了它的优点。我不再重复了。只是一些可能对你有用的点。

通常,记忆是一种操作,您可以将其应用于任何计算(昂贵)并返回值的函数。因此,它通常被实现为decorator。实现很简单,大概是这样的

memoised_function = memoise(actual_function)

或表示为装饰器

@memoise
def actual_function(arg1, arg2):
   #body

【讨论】:

【参考方案4】:

我发现这非常有用

from functools import wraps


def memoize(function):    
    memo = 
        
    @wraps(function)
    def wrapper(*args):

        # add the new key to dict if it doesn't exist already  
        if args not in memo:
            memo[args] = function(*args)

        return memo[args]

    return wrapper
    
    
@memoize
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n - 1) + fibonacci(n - 2)
    
fibonacci(25)

【讨论】:

请参阅docs.python.org/3/library/functools.html#functools.wraps,了解为什么应该使用functools.wraps 是否需要手动清除memo 以便释放内存? 整个想法是将结果存储在会话内的备忘录中。 IE。什么都没有被清除【参考方案5】:

记忆化是保留昂贵计算的结果并返回缓存的结果,而不是不断地重新计算它。

这是一个例子:

def doSomeExpensiveCalculation(self, input):
    if input not in self.cache:
        <do expensive calculation>
        self.cache[input] = result
    return self.cache[input]

更完整的描述可以在wikipedia entry on memoization找到。

【讨论】:

嗯,现在如果那是正确的 Python,它会摇滚,但它似乎不是......好吧,所以“缓存”不是一个字典?因为如果是,它应该是 if input not in self.cacheself.cache[input]has_key 已经过时了,因为......在 2.x 系列的早期,如果不是 2.0。self.cache(index) 永远不会正确。IIRC)【参考方案6】:

别忘了内置的hasattr 功能,适合那些想要手工制作的人。这样您就可以将内存缓存保留在函数定义中(而不是全局)。

def fact(n):
    if not hasattr(fact, 'mem'):
        fact.mem = 1: 1
    if not n in fact.mem:
        fact.mem[n] = n * fact(n - 1)
    return fact.mem[n]

【讨论】:

这似乎是一个非常昂贵的想法。对于每个 n,它不仅会缓存 n 的结果,还会缓存 2 ... n-1 的结果。【参考方案7】:

Memoization 基本上是保存过去使用递归算法完成的操作的结果,以便在以后需要相同计算时减少遍历递归树的需要。

见http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/

Python 中的斐波那契记忆示例:

fibcache = 
def fib(num):
    if num in fibcache:
        return fibcache[num]
    else:
        fibcache[num] = num if num < 2 else fib(num-1) + fib(num-2)
        return fibcache[num]

【讨论】:

要获得更高的性能,请使用前几个已知值预先植入您的 fibcache,然后您可以从代码的“热路径”中获取额外的逻辑来处理它们。【参考方案8】:

记忆化是将函数转换为数据结构。通常,人们希望转换以增量和惰性的方式发生(根据给定域元素或“键”的需求)。在惰性函数式语言中,这种惰性转换可以自动发生,因此可以在没有(显式)副作用的情况下实现记忆。

【讨论】:

【参考方案9】:

好吧,我应该先回答第一部分:什么是记忆?

这只是一种用记忆换时间的方法。想想Multiplication Table。

在 Python 中使用可变对象作为默认值通常被认为是不好的。但如果明智地使用它,实现memoization 实际上会很有用。

这是一个改编自http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects的例子

在函数定义中使用可变的dict,可以缓存中间计算结果(例如在计算factorial(9)之后计算factorial(10)时,我们可以重用所有中间结果)

def factorial(n, _cache=1:1):    
    try:            
        return _cache[n]           
    except IndexError:
        _cache[n] = factorial(n-1)*n
        return _cache[n]

【讨论】:

【参考方案10】:

这里有一个解决方案,可以使用 list 或 dict 类型的参数而不会抱怨:

def memoize(fn):
    """returns a memoized version of any function that can be called
    with the same list of arguments.
    Usage: foo = memoize(foo)"""

    def handle_item(x):
        if isinstance(x, dict):
            return make_tuple(sorted(x.items()))
        elif hasattr(x, '__iter__'):
            return make_tuple(x)
        else:
            return x

    def make_tuple(L):
        return tuple(handle_item(x) for x in L)

    def foo(*args, **kwargs):
        items_cache = make_tuple(sorted(kwargs.items()))
        args_cache = make_tuple(args)
        if (args_cache, items_cache) not in foo.past_calls:
            foo.past_calls[(args_cache, items_cache)] = fn(*args,**kwargs)
        return foo.past_calls[(args_cache, items_cache)]
    foo.past_calls = 
    foo.__name__ = 'memoized_' + fn.__name__
    return foo

请注意,通过在handle_item 中实现您自己的哈希函数作为特例,这种方法可以自然地扩展到任何对象。例如,要使这种方法适用于将集合作为输入参数的函数,您可以添加到 handle_item:

if is_instance(x, set):
    return make_tuple(sorted(list(x)))

【讨论】:

不错的尝试。不用抱怨,[1, 2, 3]list 参数可能会被错误地视为与值为 1, 2, 3 的不同 set 参数相同。此外,集合像字典一样是无序的,所以它们也需要是sorted()。另请注意,递归数据结构参数会导致无限循环。 是的,集合应该通过特殊的套管handle_item(x) 和排序来处理。我不应该说这个实现处理集合,因为它没有——但关键是它可以很容易地通过特殊的套管handle_item来扩展,只要它适用于任何类或可迭代对象你愿意自己编写散列函数。棘手的部分——处理多维列表或字典——已经在这里处理过了,所以我发现这个 memoize 函数作为基础比简单的“我只接受可散列参数”类型更容易使用。 我提到的问题是由于lists 和sets 被“元组化”为同一事物,因此彼此无法区分。恐怕您在最新更新中描述的添加对sets 支持的示例代码并不能避免这种情况。这可以通过分别将[1,2,3]1,2,3 作为参数传递给“memoize”d 测试函数并查看它是否被调用两次(应该调用两次)很容易看出。 是的,我读过这个问题,但我没有解决它,因为我认为它比你提到的另一个问题要小得多。你最后一次编写一个固定参数可以是列表或集合的记忆函数是什么时候,这两者导致不同的输出?如果您遇到这种罕见的情况,您将再次重写 handle_item 以添加,如果元素是集合则说 0,如果是列表则说 1。 实际上,lists 和 dicts 也有类似的问题,因为 list可能包含完全相同的内容,原因是调用make_tuple(sorted(x.items())) 获取字典。对于这两种情况,一个简单的解决方案是在生成的元组中包含 type() 的值。我可以想出一个更简单的方法来专门处理sets,但它并不能一概而论。【参考方案11】:

同时使用位置和关键字参数的解决方案与传递关键字参数的顺序无关(使用inspect.getargspec):

import inspect
import functools

def memoize(fn):
    cache = fn.cache = 
    @functools.wraps(fn)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(fn).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(fn).args)
        if key not in cache:
            cache[key] = fn(**kwargs)
        return cache[key]
    return memoizer

类似问题:Identifying equivalent varargs function calls for memoization in Python

【讨论】:

【参考方案12】:

只是想补充已经提供的答案,Python decorator library 有一些简单而有用的实现,它们也可以记忆“不可散列的类型”,这与 functools.lru_cache 不同。

【讨论】:

这个装饰器不memoize "unhashable types"!它只是回退到在没有记忆的情况下调用函数,违背了显式优于隐式教条。【参考方案13】:
cache = 
def fib(n):
    if n <= 1:
        return n
    else:
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]

【讨论】:

您可以简单地使用if n not in cache。使用cache.keys 会在 python 2 中构建一个不必要的列表

以上是关于什么是 memoization 以及如何在 Python 中使用它?的主要内容,如果未能解决你的问题,请参考以下文章

thinking--javascript 中如何使用记忆(Memoization )

thinking--javascript 中如何使用记忆(Memoization )

thinking--javascript 中如何使用记忆(Memoization )

thinking--javascript 中如何使用记忆(Memoization )

如何编写通用的 memoize 函数?

如何使用 memoization 计算二项式系数?