Recently, the StackOverflow community has helped me develop a rather concise @memoize decorator that can decorate not only functions, but also methods and classes in a general way, i.e. without any foresight of decorating.
One of the problems I ran into is that if you decorated a class with @memoize and then tried to decorate one of its methods with @staticmethod , it would not work as expected, i.e. you would not be able to call ClassName.thestaticmethod() at all. The original solution that I came up with looked like this:
def memoize(obj): """General-purpose cache for classes, methods, and functions.""" cache = obj.cache = {} def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key]
But then I found out about functools.wraps , which is designed to make the decorator function masquerade as a decorated function in a much cleaner and more complete way, and I really accepted it like this:
def memoize(obj): """General-purpose cache for class instantiations, methods, and functions.""" cache = obj.cache = {} @functools.wraps(obj) def memoizer(*args, **kwargs): """Do cache lookups and populate the cache in the case of misses.""" key = args[0] if len(args) is 1 else args if key not in cache: cache[key] = obj(*args, **kwargs) return cache[key] return memoizer
Although this looks very good, functools.wraps provides absolutely no support for either staticmethod or classmethod s. For example, if you tried something like this:
@memoize class Flub: def __init__(self, foo): """It is an error to have more than one instance per foo.""" self.foo = foo @staticmethod def do_for_all(): """Have some effect on all instances of Flub.""" for flub in Flub.cache.values(): print flub.foo Flub('alpha') is Flub('alpha') #=> True Flub('beta') is Flub('beta') #=> True Flub.do_for_all() #=> 'alpha' # 'beta'
This will work with the first implementation of @memoize , which I have listed, but will raise a TypeError: 'staticmethod' object is not callable with the second.
I really wanted to solve this simply by using functools.wraps without returning this __dict__ ugliness, so I actually redefined my own staticmethod in pure Python, which looked like this:
class staticmethod(object): """Make @staticmethods play nice with @memoize.""" def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): """Provide the expected behavior inside memoized classes.""" return self.func(*args, **kwargs) def __get__(self, obj, objtype=None): """Re-implement the standard behavior for non-memoized classes.""" return self.func
And this, as far as I can tell, works great next to the second @memoize implementation, which I list above.
So my question is: why does the standard built-in staticmethod behave correctly as it sees fit and / or why doesn't functools.wraps expect this situation and solve it for me?
Is this a bug in Python? Or in functools.wraps ?
What are the warnings about overriding the built-in staticmethod ? As I said, now it works fine, but I'm afraid there might be some hidden incompatibility between my implementation and the embedded implementation, which may explode later.
Thanks.
Edit to clarify: in my application, I have a function that performs an expensive search and is often called, so I remembered it. It is pretty simple. In addition to this, I have several classes that represent files, and having multiple instances representing the same file in the file system usually results in an inconsistent state, so it is important to force only one instance for the file name. In essence, itβs trivial to adapt the @memoize decorator for this purpose and preserve functionality as a traditional memoizer.
Real world examples in three different @memoize applications @memoize given here: