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 :-)

2 comments:

  1. You may have a look at plone.memoize, it's been here for years.

    http://pypi.python.org/pypi/plone.memoize

    In particular plone.memoize.instance

    I didn't check if it has dependencies on other plone or zope components, but you can always extract the presumably well-written, interesting parts.

    Maybe interesting too (and more 'core' python): http://pypi.python.org/pypi/memojito

    ReplyDelete
  2. This in itself is a very good practice, it limits the number of responsibilities for your class and allows greater flexibility in adding or removing the cache.

    You should also consider an expiration of your cached data, as the cached data could change every once in a while. This could be an option for your decorator?

    If the decorated method has parameters, you could use the parameters to build a key for a dictionary, and store the cached data there.

    ReplyDelete