Primary image for Django Patterns: Fat Models and cached_property

Django Patterns: Fat Models and cached_property

One of my favorite patterns in Django is the combination of “fat” models and cached_property from django.utils.functional.

Fat models are a general MVC concept which encourages pushing logic into methods on your Model layer rather than the Controller (“view” in Django parlance). This has a lot of benefits. It helps maintain the DRY (Don’t Repeat Yourself) principle by making common logic easy to find/reuse and makes it easy to break the logic down into small testable units.

One problem with this approach is that as you break down your logic into smaller more reusable units, you may find yourself using them multiple times within a single response. If your methods are particularly resource intensive, it will become an unnecessary performance hit. A common place you find this is in Django templates with patterns like this:

{% if item.top_ten_reviews %}
<ul>
{% for review in item.top_ten_reviews %}
...

This will call the top_ten_reviews method twice. If that method is making database calls, you’ve now doubled them.

Enter cached_property. This decorator will cache the results of a method for the duration of the request and return it as a property when called again. This technique is known as memoization. Let’s look at a simple example:

from django.db import models
from django.utils.functional import cached_property


class Item(models.Model):
    ...
    @cached_property
    def top_ten_reviews(self):
        """Get 10 best reviews"""
        return self.review_set.order_by('-rating')[:10]
    
class Review(models.Model):
    item = models.ForeignKey(Item)
    rating = models.PositiveIntegerField()

In my code, I can lookup an Item object and reference the property item.top_ten_reviews. The first reference will build the queryset, but future references will not, instead recalling the queryset from an in-memory cache.

A couple caveats to cached_property:

  1. Like Python’s built-in property, it only works for methods without an argument (other than self).
  2. It may not be thread-safe. If the object’s data isn’t changing between threads, it’s likely safe. If you are using threads, look to the cached-property module for a thread-safe implemenation.

Wrapping up, cached_property is one of my favorite “easy wins” when working on un-optimized code. It’s a quick and relatively safe way to cut out repeated slow tasks without getting into full-blown caching and the ensuing invalidation challenges.

Peter Baumgartner

About the author

Peter Baumgartner

Peter is the founder of Lincoln Loop, having built it up from a small freelance operation in 2007 to what it is today. He is constantly learning and is well-versed in many technical disciplines including devops, …