我刚刚开始学习Python,对于“记忆化”的概念和使用方法一无所知。能否给出一个简单的示例呢?
我刚刚开始学习Python,对于“记忆化”的概念和使用方法一无所知。能否给出一个简单的示例呢?
记忆化实际上指的是基于方法输入来记住("memoization" → "memorandum" → 被记住)方法调用的结果,然后返回记住的结果而不是再次计算结果。你可以把它看作是一种方法结果的缓存。有关详细信息,请参见Cormen等人所著《算法导论》(第3版)第387页的定义。
在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
类稍微更加强大一些。
factorial_memo
的工作方式不同,因为def factorial
内部的factorial
仍然调用旧的未记忆化的factorial
。 - adamsmithif k not in factorial_memo:
,这比if not k in factorial_memo:
更易读。 - ShreevatsaRargs
是元组。def some_function(*args)
会使 args 成为一个元组。 - Adam Smithfunctools.cache
装饰器:Python 3.9 发布了一个新的函数 functools.cache
。它可以将特定参数调用函数的结果缓存到内存中,这就是记忆化。使用起来非常简单:
import functools
import time
@functools.cache
def calculate_double(num):
time.sleep(1) # sleep for 1 second to simulate a slow calculation
return num * 2
第一次调用caculate_double(5)
时,它将花费一秒钟并返回10。第二次使用相同的参数calculate_double(5)
调用函数时,它将立即返回10。
添加cache
装饰器可以确保如果函数最近已经为特定值调用过,则不会重新计算该值,而是使用缓存的先前结果。在这种情况下,它会导致巨大的速度提升,同时代码不会被缓存的细节所淹没。
(编辑:以前的示例使用递归计算斐波那契数列,但我更改了示例以防止混淆,因此出现了旧评论。)
functools.lru_cache
装饰器:如果您需要支持较旧版本的Python,则{{link1:functools.lru_cache
}}适用于Python 3.2+。默认情况下,它仅缓存最近使用的128个调用,但您可以将maxsize
设置为None
,以指示缓存永远不会过期:
@functools.lru_cache(maxsize=None)
def calculate_double(num):
# etc
fib
函数时,需要递归到基本情况之前才能进行记忆化。因此,你目前的行为基本上是可以预期的。 - Quelklefmemoised_function = memoise(actual_function)
或者表达为装饰器
@memoise
def actual_function(arg1, arg2):
#body
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)
functools.wraps
。 - anishpatel记忆化是指保留昂贵计算的结果,而不是不断重新计算它,并返回已缓存的结果。
以下是一个例子:
def doSomeExpensiveCalculation(self, input):
if input not in self.cache:
<do expensive calculation>
self.cache[input] = result
return self.cache[input]
有关记忆化的更完整描述可以在维基百科的记忆化条目中找到。
不要忘记内置的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]
记忆化是指在递归算法中保存过去操作的结果,以减少如果稍后需要进行相同计算,则无需遍历递归树。
参见http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/
Python中的Fibonacci记忆化示例:
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]
记忆化是将函数转换为数据结构的过程。通常情况下,我们希望这种转换是增量且延迟的(按需对给定的域元素或“键”进行转换)。在惰性函数语言中,这种延迟转换可以自动发生,因此可以实现记忆化而不需要(显式)副作用。
首先,我应该回答第一个问题:什么是记忆化?
它只是一种用内存换时间的方法。想想乘法表。
在Python中,使用可变对象作为默认值通常被认为是不好的。但是,如果明智地使用,它实际上可以用来实现记忆化
。
这里有一个例子,改编自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]
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
if is_instance(x, set):
return make_tuple(sorted(list(x)))
[1,2,3]
的列表参数可能会被错误地认为与一个包含{1,2,3}
值的不同集合参数相同。此外,集合与字典一样是无序的,因此它们也需要进行sorted()
排序。还要注意,递归数据结构参数会导致无限循环。 - martineaulist
和set
被“元组化”成相同的东西,因此它们变得无法区分。你最新更新中描述添加对set
支持的示例代码恐怕并没有避免这个问题。这可以通过将[1,2,3]
和{1,2,3}
分别作为参数传递给“memoize”测试函数,并观察它是否被调用两次来轻松验证。 - martineaulist
和dict
也存在类似的问题,因为一个list
可能恰好包含与调用make_tuple(sorted(x.items()))
生成的字典相同的内容。对于这两种情况的一个简单解决方案是在生成的元组中包括值的type()
。我可以想到一种更简单的方法来处理set
,但它并不具有普适性。 - martineau