Home Blog Fun with Functional Context Values in Django

DEV

Fun with Functional Context Values in Django

Posted by julian.a on Oct. 26, 2015, 10:11 a.m.

So, what I've got today is something of a neat trick I learned recently: using
functional values in a template context to avoid unnecessary database hits.

Django's template language is intentionally somewhat restrictive: for instance
you can't call arbitrary functions, or even easily look up dictionary values
from a variable key. But one thing you can do is call functions which require
no arguments. The standard use case is accessing model methods. This lets you
lets put as much logic as possible in the model instead of the view or
template. For instance you're probably familiar with this pattern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# my_app/models.py
    def Author(models.Model):
        ...

        def full_name(self):
            return '{} {}'.format(self.first_name, self.last_name)

    # my_app/templates/my_app/author_detail.html
    ...
    <h1>{{ object.full_name }}</h1>
    ...

This trick isn't limited to model methods though; you can pass any function
into a context and if it doesn't require any arguments, access its return value
from the template (yes, a method isn't the same as a function; Django's
template syntax treats them the same way though). So, lets suppose you have an
expensive query and you want to decide in the template whether or not to do it.
You can use a functional context value to do that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 # my_app/views.py
    def my_view(request):
        def expensive_query():
            return MyModel.objects.my_expensive_query()

        return render('my_template.html', {'expensive_query_data': expensive_query})

    # my_app/templates/my_template.html
    {% if we_need_the_expensive_query %}
        {{ expensive_query_data }}
    {% else %}
        No query evaluated!
    {% endif %}

Of course, you could (and probably should) avoid the need to do this by putting
the conditional logic in the view (or better yet the model!), so this
particular trick isn't useful that often.  But suppose you have some
information from the database tha you want on many or most of your pages, but
not on all of them. For instance, what if you have a shopping cart and you want
to display the number of items in the cart on most pages, but some pages use a
different layout and don't use the cart. The natural solution is to use a
context processor:

1
2
3
4
# my_app/context_processors.py
    def cart(request):
        cart = Order.objects.get_cart(request.user)
        return {'cart': cart}

The problem with this is that if get_cart hits the database, it will do that
every page load even though we know some pages don't use it. You could make sure
get_cart is lazy (by default most querysets will be lazy after all), but that
won't always be easy or clean to do.  You could use an assignment tag, but
depending on your template layout this might create a lot of boilerplate {%
load my_tags' %}
and {% cart_tag as cart %} tags. Using functional context
values you have a clean and simple solution:

1
2
3
4
5
6
# my_app/context_processors.py
    def cart(request):
        def get_cart():
            return Order.objects.get_cart(request.user)

        return {'cart': get_cart}

Now you have a cart value in the context that you can use just like any other
context variable, but which will only hit the database if you use it. Our code
is a little more complex than it was before, but for the most part it's pretty
clear what's going on, and importantly, the logic is nicely contained in a
context processor which presents the expected interface, and has short enough
code that it won't be hard to understand six months down the line when you or a
coworker opens up the code to fix some bug or other. 

There is one catch to this approach: if you use the cart more than once in the
template you're going to see repeated calls to get_cart and as a result
repeated queries. If you are making repeated use of your variable, maybe it's
time to consider an alternative approach (maybe it's not so hard to make
Order.objects.get_cart lazy after all?) but if that's really just
unmanageable or if you're looking to score some extra points for clever python
tricks, one possible solution is to memoize the function:

1
2
3
4
5
6
7
8
9
 # my_app/context_processors.py
    def cart(request):
        memo = {}
        def get_cart():
            if not memo.get('cart'):
                memo['cart'] = Order.objects.get_cart(request.user)
            return memo['cart']

        return {'cart': get_cart}

The get_cart closure with its associated memo gets recreated each time
the `cart` context processor is called, i.e. once per request, which is just
what you want. So you have a context value which will hit the DB only once per
request, and only if you use it. I'm not sure I can wholeheartedly endorse this
last version; your coworker reading this code in six months may not appreciate
your cleverness if he has to spend half an hour convincing himself that his bug
isn't in fact being caused by your memo somehow persisting across requests.
Still, it is kind of cool!