Is there a pythonic way to support keyword arguments for memoize decorator in Python?

So, I recently asked a question about memoization and got great answers, and now I want to go to the next level. After a fairly large number of search queries, I could not find a link to the implementation of the memoize decorator, which was able to cache a function that used keyword arguments. In fact, most of them simply used *args as the key to search in the cache, which would mean it would break if you wanted to memoize a function that took lists or dicts as arguments.

In my case, the first argument to the function is a unique identifier in itself, suitable for use as a key key for finding a cache, however I need the ability to use keyword arguments and still access the same cache. I mean that my_func('unique_id', 10) and my_func(foo=10, func_id='unique_id') should return the same cached result.

To do this, we need a clean and pythonic way of saying "check kwargs for which keyword this matches the first argument". Here is what I came up with:

 class memoize(object): def __init__(self, cls): if type(cls) is FunctionType: # Let 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 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] 

The crazy thing is that it really works. For example, if you decorate like this:

 @memoize class FooBar: instances = {} def __init__(self, unique_id, irrelevant=None): print id(self) 

Then, from your code, you can call either FooBar('12345', 20) or FooBar(irrelevant=20, unique_id='12345') and actually get the same FooBar instance. Then you can define a different class with a different name for the first argument, because it works in a general way (that is, the decorator does not need to know anything specific about the class that it decorates for this to work).

The problem is that this is an unholy mess; -)

This works because inspect.getcallargs returns a dictation matching specific keywords with the arguments you supply to it, so I supply it with fake arguments and then check the dict for the first argument passed.

What would be much better if such a thing even existed is an analogue of inspect.getcallargs , which returns both types of arguments, unified as a list of arguments, and not as an argument to keyword arguments. This will do something like this:

 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] 

Another way I can handle this would be to use the dict provided by inspect.getcallargs as the search key directly, but this will require a second way to make identical lines from the same hashes, which I cannot rely on (I think I would have to build the string myself after sorting the keys).

Does anyone have any thoughts on this? Do you want to call a function with keyword arguments incorrectly and cache the results? Or just really hard?

+7
source share
2 answers

I would suggest something like the following:

 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())) 

Example:

 @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 

Note that you can also extend key_memoized and override its key() method to provide more specific memoization strategies, for example. to ignore some arguments:

 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') 
+4
source

Try lru_cache :

@functools.lru_cache(maxsize=128, typed=False)

Decorator for wrapping a function using memoizing callable, which saves to the most recent maxsize calls. This can save time when an expensive or I / O-related function is periodically called with the same arguments.

lru_cache added in python 3.2, but can be sent back in 2.x

+3
source

All Articles