Dynamic attribute, thanks to magic methods.
Tags: python, django
Published: 2019-07-26 07:32:26

# 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):

```python
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):

```python
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`:

```python
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:

```python
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