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
Top comments (0)