Caching Instance Attributes on Python ORM Objects
This is the first post in a series about our engineering experience in building a brand marketing and retail analytics platform.
So, you’ve got some expensive computations…
You’ve probably seen something like this before in a Python tutorial:
_my_prop = None @property def my_prop(self): if self._my_prop is None: self._my_prop = self.some_calculation return self._my_prop
At first you thought “okay, I get it, but when am I ever going to need that?”, but then you wrote a bunch of code, and you didn’t worry too much about performance (because premature optimization is bad), until you realized you were calling an expensive function many times…with the same input. And you thought “hurray, I’m going to use that pattern I’ve read about so many times!” And finally, you realized that you really didn’t want to change all of your functions to use that pattern. What do you do?
You write a decorator!
Something like this:
def cached_property(fn):
"""
Use like:
@property
@cached_property
def my_func(self):
return self.expensive_calculation
"""
prop = f"_{fn.__name__}"
@functools.wraps(fn)
def wrapped_fn(self, *args, **kwargs):
if not hasattr(self, prop) or getattr(self, prop) is None:
setattr(self, prop, fn(self, *args, **kwargs))
return getattr(self, prop)
return wrapped_fn
Then, probably because I named it “cached_property”, I was asked about how I was doing cache expiry, and in what cases these attributes are persistent. Here’s the answer I gave:
Attributes like this are persistent on an instance of the class.
So if we had a class like:
class Friend:
@property
@cached_property
def my_prop(self):
return "Hello Friend!"
Every instance of Friend
would run the function once before using the cached value. Ie, every time you go f = Friend()
But wait! Isn’t that different for ORM classes? Shortly: no.
ORM classes are no different than any other classes in this regard. The only difference is that some of their attributes are fetched from the database upon instantiation - ie, every time you go f = Friend()
So if (in Django ORM speak) we went:
c1 = Campaign.objects.get(id=1)
and then some time later went:
c2 = Campaign.objects.get(id=1)
we have two instances of the Campaign class. They happen to point to the same row in the database, but that doesn’t make a difference for these regular attributes. The cached_properties
would be computed separately for each one.
So the answer to the original question “in what cases are the attributes persisted?” is that the property values persist on c1
for as long as c1
exists, but they will never extend to c2
. In our case, c2
would actually be created on a separate API call, but it doesn’t actually matter when it’s created.
This is one of the reasons that using an ORM is so powerful. It lets you use all of the OOP features that your language provides, in addition to the magic of persistent values between instantiations (ie, the ones stored in the database).