Using the request cache to memoize heavy functions/methods

Memoization is a technique for caching the result of a function based on the function's input parameter.  As an example, if you're running an expensive database query where the only input is a particular object ID to lookup, you might want to cache that for subsequent lookups.

Typically, we don't want to cache these sorts of things in perpetuity, because they might change in the future.  However, during a single request, we generally do not expect these items to change and so it's perfectly acceptable to cache them.

We have a helper class, called RequestCache, which allows us to cache things in memory for the duration of a request.  Built on top of that, we have the request_cached decorator.  This decorator lets you automatically memoize a function on a per-request basis.

Let's check out a simple example:

def get_team(commentable_id):
    """ Returns the team that the commentable_id belongs to if it exists. Returns None otherwise. """
        team = CourseTeam.objects.get(discussion_topic_id=commentable_id)
    except CourseTeam.DoesNotExist:
        team = None

    return team

This function takes a single parameter, and uses that to run a database query.  By adding the request_cached decorator to it, any subsequent invocations of this function in the same request, with the same parameter, will avoid going back to the database and will instead used the cached value.

In addition, this decorator will consider all arguments – both positional and keyword – as part of the uniqueness of a function call.  Thus, if you had a function which looked up some data and then formatted it based on a keyword argument, that might not be a good fit for the decorator.  You might consider splitting up the data retrieval and the formatting into separate functions, so that the data retrieval could be request cached independently.

Warnings and pitfalls!

This biggest warning/pitfall of memoizing a function is that you should not do it if your function is not pure, or said another, if your function changes state.  As an example, if your function increments a counter or changes a value every time its called, memoizing it would prevent that from happening after the first call.  In this case, you might follow the same recommendation as above and split out the expensive code into a separate function that could be memoized.