views:

322

answers:

3

I'm looking to build a caching decorator that given a function caches the result of the function to a location specified in the decoration. Something like this:

@cacheable('/path/to/cache/file')
def my_function(a, b, c):
    return 'something'

The argument to the decorator is completely separate from the argument to the function it's wrapping. I've looked at quite a few examples but I'm not quite getting how to do this - is it possible to have an argument for the decorator that's unrelated to and not passed to the wrapped function?

+5  A: 

The idea is that your decorator is a function returning a decorator.

FIRST Write your decorator as if you knew your argument was a global variable. Let's say something like:

-

def decorator(f):
  def decorated(*args,**kwargs):
      cache = Cache(cachepath)
      if cache.iscached(*args,**kwargs):
       ...
      else:
       res = f(*args,**kwargs)
       cache.store((*args,**kwargs), res)
       return res
  return decorated

THEN Write a function that takes cachepath as an arg and return your decorator.

-

def cache(filepath)
    def decorator(f):
      def decorated(*args,**kwargs):
       cache = Cache(cachepath)
       if cache.iscached(*args,**kwargs):
        ...
       else:
        res = f(*args,**kwargs)
        cache.store((*args,**kwargs), res)
        return res
      return decorated
    return decorator
poulejapon
+3  A: 

Yes it is. As you know, a decorator is a function. When written in the form:

def mydecorator(func):
   def wrapper(*args, **kwargs):
       return func(*args, **kwargs)
   return wrapper

@mydecorator
def foo(a, b, c):
    pass

the argument passed to mydecorator is the function foo itself.

When the decorator accepts an argument, the call @mydecorator('/path/to') is actually going to call the mydecorator function with '/path/to' first. Then the result of the call to mydecorator(path) will be called to receive the function foo. You're effectively defining a dynamic wrapper function.

In a nutshell, you need another layer of decorator functions.

Here is this slightly silly example:

def addint(val):
    def decorator(func):
        def wrapped(*args, **kwargs):
            result = func(*args, **kwargs)
            return result + val
        return wrapped # returns the decorated function "add_together"
     return decorator # returns the definition of the decorator "addint"
                      # specifically built to return an extra 5 to the sum

@addint(5)
def add_together(a, b):
    return a + b

print add_together(1, 2)
# prints 8, not 3
Jarret Hardie
+3  A: 

Paul's answer is good, I would move the cache object so it doesn't need to be built every time, and design your cache so that it raises KeyError when there is a cache miss:

def cache(filepath):
    def decorator(f):
        f._cache = Cache(cachepath)
        def decorated(*args,**kwargs):
            try:
                key = (args, kwargs)
                res = f._cache.get(key)
            except KeyError:
                res = f(*args, **kwargs)
                f._cache.put(key, res)
            return res
        return decorated
    return decorator
Joel