所以我最近问了一个关于记忆的问题,得到了一些很好的答案,现在我想把它提升到一个新的水平。经过相当多的谷歌搜索,我找不到能够缓存接受关键字参数的函数的记忆装饰器的参考实现。事实上,他们中的大多数只是使用 *args
作为缓存查找的键,这意味着如果你想记住一个接受列表或字典作为参数的函数,它也会中断。
就我而言,该函数的第一个参数本身就是一个唯一标识符,适合用作缓存查找的字典键,但是我希望能够使用关键字参数并仍然访问相同的缓存。我的意思是,my_func('unique_id', 10)
和 my_func(foo=10, func_id='unique_id')
都应该返回相同的缓存结果。
为了做到这一点,我们需要的是一种干净且pythonic的方式,即"检查kwargs是否与第一个参数对应的关键字)"。这就是我想出的:
class memoize(object):
def __init__(self, cls):
if type(cls) is FunctionType:
# Let's just pretend that the function you gave us is a class.
cls.instances = {}
cls.__init__ = cls
self.cls = cls
self.__dict__.update(cls.__dict__)
def __call__(self, *args, **kwargs):
"""Return a cached instance of the appropriate class if it exists."""
# This is some dark magic we're using here, but it's how we discover
# that the first argument to Photograph.__init__ is 'filename', but the
# first argument to Camera.__init__ is 'camera_id' in a general way.
delta = 2 if type(self.cls) is FunctionType else 1
first_keyword_arg = [k
for k, v in inspect.getcallargs(
self.cls.__init__,
'self',
'first argument',
*['subsequent args'] * (len(args) + len(kwargs) - delta)).items()
if v == 'first argument'][0]
key = kwargs.get(first_keyword_arg) or args[0]
print key
if key not in self.cls.instances:
self.cls.instances[key] = self.cls(*args, **kwargs)
return self.cls.instances[key]
疯狂的是,这实际上有效。例如,如果你这样装饰:
@memoize
class FooBar:
instances = {}
def __init__(self, unique_id, irrelevant=None):
print id(self)
然后,从您的代码中,您可以调用FooBar('12345', 20)
或FooBar(irrelevant=20, unique_id='12345')
,并实际获得相同的 FooBar 实例。然后,您可以为第一个参数定义一个具有不同名称的不同类,因为它以一般方式工作(即,装饰器不需要知道有关它正在修饰的类的任何特定信息即可使其工作)。
问题是,这是一个不敬虔的混乱;-)
它之所以有效,是因为inspect.getcallargs
返回一个字典,将定义的关键字映射到您提供的参数,因此我为其提供了一些虚假参数,然后检查字典中传递的第一个参数。
如果这样的东西存在,那会更好,类似于inspect.getcallargs
,它返回两种类型的参数统一为参数列表,而不是关键字参数的字典。这将允许这样的事情:
def __call__(self, *args, **kwargs):
key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1]
if key not in self.cls.instances:
self.cls.instances[key] = self.cls(*args, **kwargs)
return self.cls.instances[key]
我可以看到解决这个问题的另一种方法是直接使用 inspect.getcallargs
提供的字典作为查找缓存键,但这需要一种可重复的方式来从相同的哈希中制作相同的字符串,这是我听说不能依赖的(我想我必须在对键进行排序后自己构造字符串)。
有人对此有什么想法吗?想要调用带有关键字参数的函数并缓存结果是错误的吗?还是非常困难?
我会建议如下:
import inspect
class key_memoized(object):
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **kwargs):
key = self.key(args, kwargs)
if key not in self.cache:
self.cache[key] = self.func(*args, **kwargs)
return self.cache[key]
def normalize_args(self, args, kwargs):
spec = inspect.getargs(self.func.__code__).args
return dict(kwargs.items() + zip(spec, args))
def key(self, args, kwargs):
a = self.normalize_args(args, kwargs)
return tuple(sorted(a.items()))
例:
@key_memoized
def foo(bar, baz, spam):
print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam)
return bar + baz + spam
print foo(1, 2, 3)
print foo(1, 2, spam=3) #memoized
print foo(spam=3, baz=2, bar=1) #memoized
请注意,您还可以扩展key_memoized
并覆盖其key()
方法以提供更具体的记忆策略,例如忽略某些参数:
class memoize_by_bar(key_memoized):
def key(self, args, kwargs):
return self.normalize_args(args, kwargs)['bar']
@memoize_by_bar
def foo(bar, baz, spam):
print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam)
return bar
print foo('x', 'ignore1', 'ignore2')
print foo('x', 'ignore3', 'ignore4')
尝试lru_cache:
@functools.lru_cache(maxsize=128, typed=False)
装饰器,用于将函数包装为具有记忆可调用对象,该可调用对象最多可保存最大大小的最新调用。当使用相同的参数定期调用昂贵或 I/O 绑定函数时,它可以节省时间。
lru_cache Python 3.2 中添加的,但可以向后移植到 2.x 中
你可以看看我的包: https://github.com/Yiling-J/cacheme,实际上我为所有参数/kwargs 使用容器:
@cacheme(key=lambda c: 'cat:{name}'.format(name=c.cat.name))
def get_cat(self, cat):
return some_function(cat)