I made a helper decorator for this purpose:
from functools import update_wrapper
class ClassWrapper:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
class ClassWrapperInner:
def __init__(self, cls, *args, **kwargs):
# This combines previous information to get ready to recieve the actual function in the __call__ method.
self._cls = cls
self.args = args
self.kwargs = kwargs
def __call__(self, func, *args, **kw):
# Basically "return self._cls(func, *self.args, **self.kwargs)", but with an adjustment to update the info of the new class & verify correct arguments
assert len(args) == 0 and len(kw) == 0 and callable(func), f"{self._cls.__name__} got invalid arguments. Did you forget to parenthesize?"
obj = self._cls(func, *self.args, **self.kwargs)
update_wrapper(obj, func)
return obj
return ClassWrapperInner(self.cls, *args, **kwargs)
This weird code makes more sense in the context of how it will be executed:
double = ClassWrapper(Cache)(max_hits=100, timeout=50)(double)
ClassWrapper.__init__
stores the class it will be wrapping, (Cache).
ClassWrapper.__call__
passes on its arguments (max_hits=100, timeout=50) to ClassWrapperInner.__init__
, which stores them for the next call.
ClassWrapper.__call__
combines all of the previous arguments and (func) together and gives them to an instance of your class, Cache
, which it returns for use as the new double
. It also updates your class's arguments, __name__
and __doc__
with the functools library. It's kind of like a way more complicated version of 2d list flattening where it's function arguments instead of lists.
With this class decorating it, your original function behaves as expected, except that you need to put parentheses around it in all cases.
@ClassWrapper
class Cache(object):
def __init__(self, function, max_hits=10, timeout=5):
self.function = function
self.max_hits = max_hits
self.timeout = timeout
self.cache = {}
def __call__(self, *args):
... # Here the code returning the correct thing.
@Cache()
def double(x):
return x * 2
@Cache(max_hits=100, timeout=50)
def double(x):
return x * 2
You could try to edit ClassWrapperInner.__call__
so that the parentheses are not required, but this approach is hacky and doesn't really make sense; it's like trying to add logic to each method of a class so that calling them without a self parameter works correctly.
EDIT:
After writing this answer, I realized there was a much better way to make the decorator:
def class_wrapper(cls):
def decorator1(*args, **kwargs):
def decorator2(func):
return cls(func, *args, **kwargs)
return decorator2
return decorator1
With functools functions for updating the name & things:
def class_wrapper(cls):
def decorator1(*args, **kwargs):
@wraps(cls)
def decorator2(func):
obj = cls(func, *args, **kwargs)
update_wrapper(obj, func)
return obj
return decorator2
return decorator1
Cache
with positional instead of keyword arguments (e.g.@Cache(100,50)
) thenfunction
will be assigned the value 100, andmax_hits
50. An error won't be raised until the function is called. This could be considered surprising behavior since most people expect uniform positional and keyword semantics. – Martell