Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python - How to specify an optional argument for class based decorators?

How would I write a decorator like this. I want to be able to specify the value for max_hits when I call the decorator (or optionally leave it out).

E.g., the desired use would be

@memoize(max_hits=7)
def a(val):
    print val

or

@memoize
def a(val):
    print val

(Using the first example gives an error about incorrect arguments.)

Decorator:

class memoize:
    """A decorator to cache previosly seen function inputs.

    usage:
        @memoize
        def some_func(..
    """
    def __init__(self, function, max_hits=None):
        self.max_hits = max_hits
        self.function = function
        self.memoized = {}

    def __call__(self, *args, **kwargs):
        key = (args,tuple(kwargs.items()))
        try:
            return self.memoized[key]
        except KeyError:
            self.memoized[key] = self.function(*args,**kwargs)
        return self.memoized[key]
like image 270
Greg Avatar asked Dec 16 '22 14:12

Greg


2 Answers

You have to make memoize a function that takes an optional argument max_hits and returns a decorator (i.e. another callable object that will take the function as the first argument); in this case, you can use the following two syntaxes:

@memoize()
def func(x):
    [...]

@memoize(max_hits=7)
def func(x):
    [...]

So, probably something along the lines of:

def memoize(max_hits=None):
    """Returns a decorator to cache previosly seen function inputs.

    usage:
    @memoize()
    def some_func(..
    """
    class decorator:
        def __init__(self, function):
            self.max_hits = max_hits
            self.function = function
            self.memoized = {}

        def __call__(self, *args, **kwargs):
            key = (args,tuple(kwargs.items()))
            try:
                return self.memoized[key]
            except KeyError:
                self.memoized[key] = self.function(*args,**kwargs)
            return self.memoized[key]

    return decorator

Note that @memoize() will work but your originally desired @memoize syntax won't; in the latter case, the function you are decorating with @memoize would be passed to memoize as the first argument (max_hits). If you want to treat this case, you can extend memoize as follows:

def memoize(max_hits=None):
    if callable(max_hits):
        # For sake of readability...
        func = max_hits
        decorator = memoize(max_hits=None)
        return decorator(func)
    [...original implementation follows here...]
like image 71
Tamás Avatar answered May 09 '23 11:05

Tamás


If you're using 3.2+, don't write it yourself, use from functools import lru_cache instead

If you want to support the no-parenthesis version, it is also better to use a sentinel value for the function argument rather than introspecting the "wrong" argument. That is:

class Memoized(object):
    # As per the memoize definition in the question

# Leave out the "*, " in 2.x, since keyword-only arguments are only in 3.x
from functools import partial
_sentinel = object()
def memoize(_f=_sentinel, *, max_hits=None):
    if _f is _sentinel:
        return partial(Memoized, max_hits=max_hits)
    return Memoized(_f, max_hits=max_hits)
like image 26
ncoghlan Avatar answered May 09 '23 09:05

ncoghlan