Sunday, January 25, 2015

Django Tip: Using the @property decorator on custom model methods

Using the @property decorator allows you to trigger custom model methods as if they were normal model attributes. For example, if you have the following custom model method:
class MyModel(object):
    def combined_name(self):
        return self.first_name + ' ' + self.last_name
Adding the @property decorator to that method allows you to access the computed value of combined_name like an attribute: MyModel().combined_name, instead of having to explicitly trigger the function like: MyModel().combined_name().

How does the @property decorator work?


There's actually a lot going on behind the scenes in Python to make this happen. First of all, the @property decorator is just a convenient way to call the property() function, which is built in to Python. This function returns a special descriptor object which allows direct access to the method's computed value. So instead of doing:
class FooBar(object):
    def foo(self):
        return "foo"
    
    x = property(foo)
You can do:
class FooBar(object):
    @property
    def foo(self):
        return "foo"
Both implementations allow you to access the computed value, the string "foo" in this case, by either doing FooBar().x or FooBar().foo. And again, this is possible because the attribute is now pointing to a descriptor object which is triggering the method foo(self) and returning the computed value.

Descriptor objects have one or more of the following methods defined: __get__, __set__, and/or __delete__. Using the property() function, you can set these methods by passing them in as arguments like so: property(fget, fset, fdel), with fget getting assigned to __get__, the fset to __set__, and fdel to __delete__. In our above examples, we're only passing in the fget positional argument. So, when you access the class attribute assigned to the descriptor object, the method we passed in is now assigned to the descriptor object's __get__ method and is triggered, with the computed value being returned.

These special methods also have access to the instance. Which is how something like the following could work:
class FooBar(object):
    def __init__(self, name='Dylan'):
        self.name = name
    
    @property
    def foo(self):
        return "My name is " + self.name + "!"
Which is why calling FooBar(name='Mark').foo would evaluate to "My name is Mark!"

Confused still? That's fine. I am too at times. This isn't the easiest of stuff to wrap your head around. Take a look at the "Further Reading" below for more helpful resources about descriptor objects and the property function.

Why use it?


I only recently stumbled upon the @property decorator. Personally, I like to use it for model methods that return computed attributes. For example, in my project www.wikivinci.com, I have an Account model. Each Account instance has a 'points' IntegerField to, well, keep track of each Account's total points. I wrote a basic model method to return the ranking of an Account instance (ie: how many other Account objects are in the database with higher 'points'). This method simply computes an attribute, so I chose to use the @property decorator.

On the other hand, I also wrote the following model method:
def award_points(self, points=0):
    self.points += points
    self.save()
It doesn't make sense to use the @property decorator here. Since this is a method that's called like a traditional function with arguments.

Django Limitations/Considerations


While using the @property decorator can make it a bit more convenient to access model methods as attributes, you can't filter querysets by them, use them for ordering, or any other operation that happens at the database level. I read somewhere that when using the the @property decorator, the computed value is cached, but I didn't find that to be true during my testing. If someone has more insight around this, and/or other limitations or best practices regarding using custom model method properties, please post them in the comments!

Further Reading


Property function
Descriptors
How does the property decorator work - Stack Overflow

5 comments:

  1. "Both implementations allow you to access the computed value, the string "foo" in this case, by either doing FooBar().x or FooBar().foo."

    I found this did not work. I had to use FooBar().x() or FooBar().foo(), otherwise what comes back, e.g. via a print, is a

    Is some other "magic" needed?

    ReplyDelete
  2. Thanks for taking the time to discuss that, I feel strongly about this and so really like getting to know more on this kind of field. Do you mind updating your blog post with additional insight? It should be really useful for all of us. La Manga Club Property For Sale

    ReplyDelete
  3. You can override the save() method on the model class and take your @property value and update a model field with it. That way you have the latest @property saved in the database as well.

    ReplyDelete