DEV Community

Cover image for Dynamic attribute, thanks to magic methods.
Ghislain LEVEQUE
Ghislain LEVEQUE

Posted on

3 2

Dynamic attribute, thanks to magic methods.

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

Photo by Clem Onojeghuo on Unsplash

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more