Ghislain LEVEQUE
Posted on July 26, 2019
Scenario
In order to optimize our application, I added an annotation on my QuerySet in order to be able to filter on this dynamic property and do bulk operations without needing to iterate over all the objects in a QuerySet:
So I could replace that (simplified example):
class MyModel(models.Model):
attr1 = models.DateTimeField()
attr2 = models.IntegerField()
def attr3(self):
return self.attr1 + timedelta(days=attr2 * 3)
def my_function():
for instance in MyModel.objects.all():
# do some work involving attr3
By that (incomplete example):
class MyManager(models.Manager):
def get_queryset(self):
return self.custom_annotation(super(MyManager, self).get_queryset())
def custom_annotation(self, qs):
return qs.annotate(
attr3= # Details of annotation outside the scope of this post
)
class MyModel(models.Model):
attr1 = models.DateTimeField()
attr2 = models.IntegerField()
objects = MyManager()
def my_function():
MyModel.objects.filter(
attr3=... # Details of filter outside the scope of this post
).update(
# Details of operation outside the scope of this post
)
def my_other_function():
instance = MyModel.objects.get(pk=42)
instance.attr3 # the instance is annotated thanks to the QuerySet annotation
Ok, fine so far but this has a flaw. When you are accessing your model directly (via a QuerySet
or an instance), it is annotated. When you are accessing it via a ManyToMany
, it is annotated too but not when it is used as a ForeignKey
:
class MyModel2(models.Model):
foreign = models.ForeignKey(MyModel)
def my_function():
instance2 = MyModel2.objects.get(pk=57)
instance2.foreign.attr3 # AttributeError
Solution
So I wanted to have MyModel
act dynamically and go get the attr3
attribute when needed, at the cost of an additional SQL query if necessary.
This is possible by overriding __getattr__
like that:
class MyModel(models.Model):
attr1 = models.DateTimeField()
attr2 = models.IntegerField()
objects = MyManager()
def __getattr__(self, attrname):
if attrname == "attr3":
try:
return object.__getattribute__(self, attrname)
except AttributeError:
value = Screen.objects.values_list("attr3", flat=True).get(pk=self.pk)
setattr(self, attrname, value)
return value
return object.__getattribute__(self, attrname)
One should be careful when overriding __getattr__
, you can easily fall into infinite loops.
Also, it is important to note that __getattr__
only gets called when accessing a missing attribute, whereas __getattribute__
gets called every time an attribute is accessed.
I'm using object.__getattribute__
in order to avoid recursion.
Links
- Python documentation: https://docs.python.org/3/reference/datamodel.html
- A discussion about the difference between both magic methods: https://stackoverflow.com/questions/3278077/difference-between-getattr-vs-getattribute
Photo by Clem Onojeghuo on Unsplash
Posted on July 26, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024