May 12, 2017 · dsp2017 python django

How to make your Django context processors lazy

If you came here, you probably know what is a context processor, but let's quickly recap. It's a function, that returns dictionary of values. Those values will be injected to rendering context without explicitly defining them in view. They allows us to refactor commonly used variables out of views.

The most well-known context processor is Authentication, making it possible to use user and perms variable in our templates, pointing to currently authenticated user. Other useful build-in context processors are Messages and Request (full list here).

To enable context processor, we need to put fully qualified path in context_processors array in TEMPLATE setting. Let's take a look at custom context processor.

Context processor example

# context_processors.py
from models import User

def users_online(request):  
    return {
       "online_count": User.objects.online().count()
    }

usage:

# in settings.py, under TEMPLATES[0]['OPTIONS']
'context_processors': [  
    ...,  
    'project.context_processors.users_online'
]

Problem

Context processors are called every time we're rendering template. Even if provided variables aren't used, they are prepared and injected into context. Some processors may be expensive to calculate, so it may be better to postpone their execution.

Solution

Thanks to Django Template Language behaviour, it's possible to use functions as variables. When used in template, they return value will be used instead. I've written decorator to utilize that:

# utils.py
import functools

def memoize(method):  
    @functools.wraps(method)
    def memoizer(*args, **kwargs):
        method._cache = getattr(method, '_cache', {})
        key = args
        if key not in method._cache:
            method._cache[key] = method(*args, **kwargs)
        return method._cache[key]
    return memoizer

Usage:

# context_processors.py
from models import User  
from utils import memoize

def users_online(request):  
    return {
       "online_count": memoize(
           lambda: User.objects.online().count()
       )
    }

# can also be used like this
@memoize
def heavy_computations(a, b, c):  
    ...

Explanation:
Memoize remembers return value of a function call, under the key made from it's arguments. For each arguments combination it's called at most one time. In our example, we're using argument-less lambda functions, so it's called either 0 or 1 time (on first usage).

Now our "online_count" value is lazy - it's calculated when it appear for first time inside templates. If not, it's not executed, and we save one DB query. Maybe it's not that much, but when you have many context variables similar to that one, it may really make a difference.

I'm using memoize in all my context processors. If you care about performance of your application, use it too!

Comments powered by Disqus