26

I'm trying to pass optional arguments to my class decorator in python. Below the code I currently have:

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

The second decorator with arguments to overwrite the default one (max_hits=10, timeout=5 in my __init__ function), is not working and I got the exception TypeError: __init__() takes at least 2 arguments (3 given). I tried many solutions and read articles about it, but here I still can't make it work.

Any idea to resolve this? Thanks!

Dachmt
  • 1,959
  • 4
  • 28
  • 45

7 Answers7

24

@Cache(max_hits=100, timeout=50) calls __init__(max_hits=100, timeout=50), so you aren't satisfying the function argument.

You could implement your decorator via a wrapper method that detected whether a function was present. If it finds a function, it can return the Cache object. Otherwise, it can return a wrapper function that will be used as the decorator.

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.

# wrap _Cache to allow for deferred calling
def Cache(function=None, max_hits=10, timeout=5):
    if function:
        return _Cache(function)
    else:
        def wrapper(function):
            return _Cache(function, max_hits, timeout)

        return wrapper

@Cache
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2
lunixbochs
  • 20,457
  • 2
  • 36
  • 45
  • Thanks guys and @lunixbochs for your solution! Works like a charm :) – Dachmt Sep 20 '11 at 21:56
  • 7
    If the developer calls`Cache` with positional instead of keyword arguments (e.g. `@Cache(100,50)`) then `function` will be assigned the value 100, and `max_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. – unutbu Sep 20 '11 at 22:06
  • 1
    If i use the @Cache decorator on an object instance method, then the ```__call__``` method of ```_Cache``` doesn't receive the self reference of the decorated object. Doesn't work in this case. – MichaelMoser Dec 02 '21 at 02:17
  • wow. this works, on regular functions – alexzander Dec 30 '21 at 20:35
21
@Cache
def double(...): 
   ...

is equivalent to

def double(...):
   ...
double=Cache(double)

While

@Cache(max_hits=100, timeout=50)
def double(...):
   ...

is equivalent to

def double(...):
    ...
double = Cache(max_hits=100, timeout=50)(double)

Cache(max_hits=100, timeout=50)(double) has very different semantics than Cache(double).

It's unwise to try to make Cache handle both use cases.

You could instead use a decorator factory that can take optional max_hits and timeout arguments, and returns a decorator:

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.

def cache_hits(max_hits=10, timeout=5):
    def _cache(function):
        return Cache(function,max_hits,timeout)
    return _cache

@cache_hits()
def double(x):
    return x * 2

@cache_hits(max_hits=100, timeout=50)
def double(x):
    return x * 2

PS. If the class Cache has no other methods besides __init__ and __call__, you can probably move all the code inside the _cache function and eliminate Cache altogether.

unutbu
  • 777,569
  • 165
  • 1,697
  • 1,613
  • 1
    unwise or not... if the developer does accidentally use @cache instead of cache(), it'll make a weird error when they try to call the resulting function. the other implementation actually works as both cache and cache() – lunixbochs Sep 20 '11 at 21:50
  • 2
    @lunixbochs: A developer who confuses `cache_hits` (nee `cache`) with `cache_hits()` is just as likely to confuse any function object with a function call, or mistake a generator with an iterator. Even moderately experienced Python programmers should be used to paying attention to the differenc. – unutbu Sep 20 '11 at 22:22
6

I'd rather to include the wrapper inside the class's __call__ method:

UPDATE: This method has been tested in python 3.6, so I'm not sure about the higher or earlier versions.

class Cache:
    def __init__(self, max_hits=10, timeout=5):
        # Remove function from here and add it to the __call__
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, function):
        def wrapper(*args):
            value = function(*args)
            # saving to cache codes
            return value
        return wrapper

@Cache()
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2
Alex Jolig
  • 12,826
  • 19
  • 125
  • 158
  • Have you tried calling the functions after decorating them? I think this method doesn't work. – therealak12 Jun 12 '20 at 17:51
  • 1
    @AK12 Have you tried it or you just think it wont work? Cz I'm working with this method and it works fine. – Alex Jolig Jun 13 '20 at 09:44
  • I've tried and got errors. The error happens when I try to call the double method. – therealak12 Jun 13 '20 at 09:51
  • @AK12 What error do you get? Cz I just tried it in a test project with Python3.6 and it works – Alex Jolig Jun 13 '20 at 10:22
  • @AK12 Did you try copy pasting the whole answer in a new project and run for example `double(5)`? What's you python version? – Alex Jolig Jun 13 '20 at 10:50
  • I use python 3.8, I copy-pasted the __call__ method and the two decorated methods. – therealak12 Jun 13 '20 at 11:10
  • 1
    @AK12 If you have copied the whole class and the problem still exists, then I suspect the issue could be cause of our differenet Python versions. – Alex Jolig Jun 13 '20 at 11:17
  • 2
    Among all proposals in this thread, this way is the simplest and the easiest to use. Thank you @AlexJolig – Ivy Growing Jul 26 '21 at 07:29
  • 1
    Tested in Python 3.9.7, working fine! Most concise one! – Frank He Mar 05 '22 at 18:39
  • 1
    Just a note: This class is a decorator **factory**, while the `__call__` function defines the decorator itself. IMO this is much cleaner then the accepted answer. You could also add `@functools.wraps(function)` infront of your `wrapper` function in order to improve your answer even further. – M.Winkens Apr 12 '22 at 09:42
3

I've learned a lot from this question, thanks all. Isn't the answer just to put empty brackets on the first @Cache? Then you can move the function parameter to __call__.

class Cache(object):
    def __init__(self, max_hits=10, timeout=5):
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, function, *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

Although I think this approach is simpler and more concise:

def cache(max_hits=10, timeout=5):
    def caching_decorator(fn):
        def decorated_fn(*args ,**kwargs):
            # Here the code returning the correct thing.
        return decorated_fn
    return decorator

If you forget the parentheses when using the decorator, unfortunately you still don't get an error until runtime, as the outer decorator parameters are passed the function you're trying to decorate. Then at runtime the inner decorator complains:

TypeError: caching_decorator() takes exactly 1 argument (0 given).

However you can catch this, if you know your decorator's parameters are never going to be a callable:

def cache(max_hits=10, timeout=5):
    assert not callable(max_hits), "@cache passed a callable - did you forget to parenthesize?"
    def caching_decorator(fn):
        def decorated_fn(*args ,**kwargs):
            # Here the code returning the correct thing.
        return decorated_fn
    return decorator

If you now try:

@cache
def some_method()
    pass

You get an AssertionError on declaration.

On a total tangent, I came across this post looking for decorators that decorate classes, rather than classes that decorate. In case anyone else does too, this question is useful.

Chris
  • 4,608
  • 3
  • 38
  • 47
2

You can use a classmethod as a factory method, this should handle all the use cases (with or without parenthesis).

import functools
class Cache():
    def __init__(self, function):
        functools.update_wrapper(self, function)
        self.function = function
        self.max_hits = self.__class__.max_hits
        self.timeout = self.__class__.timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.
    
    @classmethod
    def Cache_dec(cls, _func = None, *, max_hits=10, timeout=5):
        cls.max_hits = max_hits
        cls.timeout = timeout
        if _func is not None: #when decorator is passed parenthesis
            return cls(_func)
        else:
            return cls    #when decorator is passed without parenthesis
       

@Cache.Cache_dec
def double(x):
    return x * 2

@Cache.Cache_dec()
def double(x):
    return x * 2

@Cache.Cache_dec(timeout=50)
def double(x):
    return x * 2

@Cache.Cache_dec(max_hits=100)
def double(x):
    return x * 2

@Cache.Cache_dec(max_hits=100, timeout=50)
def double(x):
    return x * 2
  • but you end up with the same class instance for each application of the decorator. That is a problem, as each of the decorated functions is seing the same set of parameters. – MichaelMoser Dec 01 '21 at 04:30
0

Define decorator that takes optional argument:

from functools import wraps, partial             
def _cache(func=None, *, instance=None):         
    if func is None:                             
        return partial(_cache, instance=instance)
    @wraps(func)                                 
    def wrapper(*ar, **kw):                      
        print(instance)                          
        return func(*ar, **kw)                   
    return wrapper         

And pass the instance object to decorator in __call__, or use other helper class that is instantiated on each __call__. This way you can use decorator without brackets, with params or even define a __getattr__ in proxy Cache class to apply some params.

class Cache:                                   
    def __call__(self, *ar, **kw):             
        return _cache(*ar, instance=self, **kw)
                                               
cache = Cache()                                
                                               
@cache                                         
def f(): pass                                  
f() # prints <__main__.Cache object at 0x7f5c1bde4880>

                                       

                  
Karolius
  • 433
  • 4
  • 12
0
class myclass2:
 def __init__(self,arg):
  self.arg=arg
  print("call to init")
 def __call__(self,func):
  print("call to __call__ is made")
  self.function=func
  def myfunction(x,y,z):
   return x+y+z+self.function(x,y,z)
  self.newfunction=myfunction
  return self.newfunction
 @classmethod
 def prints(cls,arg):
  cls.prints_arg=arg
  print("call to prints is made")
  return cls(arg)


@myclass2.prints("x")
def myfunction1(x,y,z):
 return x+y+z
print(myfunction1(1,2,3))

remember it goes like this:
first call return object get second argument
usually if applicable it goes like argument,function,old function arguments
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 11 '22 at 16:00