Thursday, December 15, 2011

Function caching decorator

Every once in a while, you have to create a function in one of your models, that does a lot of queries. If you use that function a couple of times, you might find it a wise thing to 'cache' the function. A 'cached' function will look like this:
def get_important_data(self):
    if not hasattr(self, _important_data):
        self._important_data = self.get_result_of_lots_of_queries()
    return self._important_data
If you call this function once on an object, it will run a lot of queries, but will store the result in self._important_data. The next time you call the function on this exact same object, the attribute self._important_data will still be set and the huge load of queries do not have to be executed again.

In some projects you have to create such a function more than once, for whatever reason. A colleague got sick and tired of writing the same code over and over again and came up with an idea to tackle this concurrency. So he asked me if, when I had little to do, I could write a decorator that does exactly the same kind of 'caching' as the function above. I however have little to no experience in writing decorators, so I copy/pasted something together from the interwebz, that seems to do what I want.

This is what I came up with.

in decorators.py:
from functools import wraps

def cached_function(attr):

    def inner_cached_function(fn):

        def return_attr(*args, **kwargs):
            try:
                cls = args[0].__class__ # args[0] should be fn's self
            except:
                raise Exception("""A function with the cached_function
                    decorator must use 'self' as first argument""")

            if not hasattr(cls, attr):
                value = fn(*args, **kwargs)
                setattr(cls, attr, value)
            return getattr(cls, attr)

        return wraps(fn)(return_attr)

    return inner_cached_function
in a ModelClass:
    @cached_function('_important_data')
    def get_important_data(self):
        return self.get_result_of_lots_of_queries()
As said/written before, I almost have no experience with this, so please comment if you have anything to say about this solution.
Thanks :-)