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
class MyModel2(models.Model): foreign = models.ForeignKey(MyModel) def my_function(): instance2 = MyModel2.objects.get(pk=57) instance2.foreign.attr3 # AttributeError
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.
object.__getattribute__ in order to avoid recursion.
- 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