什么是备忘录,如何在Python中使用备忘录?


378

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


215
当相关的维基百科文章的第二句话包含短语“相互递归下降解析[1]在通用的自上而下的解析算法[2] [3]中,该算法在多项式时空中适应歧义和左递归时”,我认为问SO发生了什么是完全适当的。
懵懵懂懂

10
@无提示:在该短语之前加上“在其他情况下(以及为了提高速度之外的其他目的),例如in中也使用了记忆”。因此,这只是示例列表(无需理解);它不是对记忆的解释的一部分。
ShreevatsaR

1
@StefanGruenwald该链接已死。你能找到一个更新吗?
JS。

2
由于pycogsci.info已关闭,因此指向pdf文件的新链接为:people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Stefan Gruenwald

4
@Clueless,该文章实际上说“ 在通用的自上而下的解析算法[2] [3]中,简单的相互递归下降解析[1]可在多项式时空中适应歧义和左递归”。您错过了simple,这显然使该示例更加清楚了:)。
studgeek '17

Answers:


353

记忆有效地指基于方法输入记忆方法调用的结果(“记忆”→“备忘录”→要记忆),然后返回记忆的结果,而不是再次计算结果。您可以将其视为方法结果的缓存。有关更多详细信息,请参见第387页,Cormen等人的算法简介(3e)中的定义。

一个简单的示例,使用Python中的记忆来计算阶乘是这样的:

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]

您可能会变得更加复杂,并将备注过程封装到一个类中:

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中添加了一个称为“ 装饰器 ”的功能,使您现在只需编写以下代码即可完成同一操作:

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

Python的装饰图书馆有一个名为类似的装饰memoized是不是稍微更强大的是Memoize这里显示类。


2
感谢您的建议。Memoize类是一个优雅的解决方案,可以轻松地应用于现有代码,而无需进行大量重构。
勒普顿船长

10
Memoize类解决方案是有缺陷的,它不能与之解决相同的问题factorial_memo,因为factorial内部def factorial仍然调用旧的unmemoize factorial
adamsmith

9
顺便说一句,您也可以编写if k not in factorial_memo:,其阅读效果优于if not k in factorial_memo:
ShreevatsaR 2014年

5
作为装饰者应该真正做到这一点。
Emlyn O'Regan 2014年

3
@ durden2.0我知道这是一个旧注释,但是args是一个元组。def some_function(*args)使args为元组。
亚当·史密斯

232

是Python 3.2的新功能functools.lru_cache。默认情况下,它仅缓存最近使用的128个调用,但是您可以将设置为maxsizeNone以指示缓存永不过期:

import functools

@functools.lru_cache(maxsize=None)
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

此功能本身非常慢,请尝试fib(36),您将需要等待大约十秒钟。

添加lru_cache注释可确保如果最近已为特定值调用了该函数,则该函数不会重新计算该值,而是使用缓存的先前结果。在这种情况下,它可以极大地提高速度,而代码不会因缓存的细节而混乱。


2
尝试fib(1000),得到了RecursionError:在比较中超出了最大递归深度
XÆA-12

5
@Andyk默认Py3递归限制为1000。第一次fib调用时,需要先递归到基本情况,然后再进行存储。因此,您的行为与预期差不多。
Quelklef

1
如果我没记错的话,它只会缓存直到进程被杀死,对吧?还是不管进程是否被杀死都缓存?例如,说我重新启动系统-缓存的结果是否仍会被缓存?
Kristada673 '18 -10-22

1
@ Kristada673是的,它存储在进程的内存中,而不是磁盘上。
Flimm

2
请注意,由于它是递归函数并且正在缓存其自身的中间结果,因此甚至可以加快函数的首次运行速度。最好说明一个非递归函数,这种函数本质上很慢,很难让像我这样的虚拟人更清楚。:D
endolith

61

其他答案涵盖了它相当不错的地方。我不再重复。只是一些对您可能有用的观点。

通常,备注是一项操作,您可以将其应用于可计算某些内容(昂贵)并返回值的任何函数。因此,通常将其实现为装饰器。实现很简单,就像这样

memoised_function = memoise(actual_function)

或表示为装饰者

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

18

备注保持了昂贵的计算结果并返回缓存的结果,而不是不断地重新计算它。

这是一个例子:

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

有关备注的详细信息,请参见Wikipedia条目


嗯,现在,如果那是正确的Python,它将摇晃,但似乎不是……好吧,因此“缓存”不是命令吗?因为如果是这样,它应该是 if input not in self.cacheself.cache[input]has_key已过时,因为......在2.x系列的早期,如果不是2.0。 self.cache(index)从来没有正确IIRC)
于尔根A.艾哈德

15

hasattr对于想手工制作的人,不要忘了内置功能。这样,您可以将mem缓存保留在函数定义内(而不是全局)。

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。
codeforester

15

我发现这非常有用

def memoize(function):
    from functools import wraps

    memo = {}

    @wraps(function)
    def wrapper(*args):
        if args in memo:
            return memo[args]
        else:
            rv = function(*args)
            memo[args] = rv
            return rv
    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.wrapsfunctools.wraps
anishpatel

1
我是否需要手动清除memo以便释放内存?

整个想法是将结果存储在会话中的备忘录中。即没有任何东西可以被清除
比耶尔先生

6

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

参见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]

2
为了获得更高的性能,请先在fibcache中添加前几个已知值,然后再使用额外的逻辑将其处理出代码的“热门路径”。
jkflying 2014年

5

记忆化是将功能转换为数据结构。通常,人们希望转换是渐进地和惰性地进行(根据给定的域元素或“键”的需求)。在惰性函数语言中,这种惰性转换可以自动发生,因此可以实现备忘录而没有(显式)副作用。


5

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

这只是以时间交换内存的一种方法。想想乘法表

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

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

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

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

4

这是一个可以使用列表或字典类型参数而无需抱怨的解决方案:

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
很好的尝试。无呜呜,一个list的参数[1, 2, 3]可能会错误地被认为是相同的不同的set,值为参数{1, 2, 3}。此外,集合像字典一样是无序的,因此它们也必须是sorted()。另请注意,递归数据结构参数将导致无限循环。
martineau 2014年

是的,集应该通过特殊的大小写handle_item(x)和排序来处理。我不应该说这个实现可以处理集合,因为它不可以处理-重点是可以通过特殊的大小写handle_item轻松扩展它,并且只要任何类或可迭代对象都可以使用您愿意自己编写哈希函数。棘手的部分-处理多维列表或字典-已在此处处理,因此我发现与基本的“我只接受可散列的参数”类型相比,此备注功能作为基础更容易使用。
RussellStewart 2014年

我提到的问题是由于一个事实,即lists和sets被“简化为”同一个事物,因此变得彼此难以区分。sets恐怕您的最新更新中描述的添加对的支持的示例代码无法避免。这可以很容易地通过分别可以看到[1,2,3]{1,2,3}作为参数传递给“memoize的” d测试功能,看它是否就是所谓的两倍,因为它应该是,还是不行。
martineau 2014年

是的,我读过这个问题,但是我没有解决,因为我认为这个问题比您提到的另一个问题小得多。您上次编写记忆功能的时间是什么时候?该功能的固定参数可以是列表或集合,而这两个参数会导致不同的输出?如果遇到这种罕见情况,您将再次重写handle_item使其成为前缀,如果元素是集合,则说为0,如果是列表,则说为1。
RussellStewart 2014年

实际上,lists和dicts 存在类似的问题,因为a 可能list具有与调用make_tuple(sorted(x.items()))字典所导致的完全相同的内容。这两种情况的简单解决方案是将type()value的值包含在生成的元组中。我可以想到一种专门用于处理sets的更简单的方法,但是并不能一概而论。
martineau 2014年

3

与位置参数和关键字参数一起使用的解决方案,与关键字args的传递顺序无关(使用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

相似的问题:确定等效的varargs函数调用以在Python中进行记忆



2

只是想添加到已经提供的答案中,Python装饰器库具有一些简单但有用的实现,与相比,它们还可以记住“无法散列的类型” functools.lru_cache


1
该修饰器不会记住“无法散列的类型”!它只是退回到没有记忆的情况下调用函数,违背显式比隐式的教条要好
ostrokach
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.