Python 201: Properties

Python has a neat little concept called a property that can do several useful things. In this article, we will be looking into how to do the following:

  • Convert class methods into read-only attributes
  • Reimplement setters and getters into an attribute

In this article, you will learn how to use the builtin class property in several different ways. Hopefully by the end of the article, you will see how useful it is.

Getting Started

One of the simplest ways to use a property is to use it as a decorator of a method. This allows you to turn a class method into a class attribute. I find this useful when I need to do some kind of combination of values. Others have found it useful for writing conversion methods that they want to have access to as methods. Let's take a look at a simple example:

########################################################################
class Person(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, first_name, last_name):
        """Constructor"""
        self.first_name = first_name
        self.last_name = last_name
    
    #----------------------------------------------------------------------
    @property
    def full_name(self):
        """
        Return the full name
        """
        return "%s %s" % (self.first_name, self.last_name)

In the code above, we create two class attributes or properties: self.first_name and self.last_name. Next we create a full_name method that has a @property decorator attached to it. This allows us to the following in an interpreter session:

>>> person = Person("Mike", "Driscoll")
>>> person.full_name
'Mike Driscoll'
>>> person.first_name
'Mike'
>>> person.full_name = "Jackalope"
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: can't set attribute

As you can see, because we turned the method into a property, we can access it using normal dot notation. However, if we try to set the property to something different, we will cause an AttributeError to be raised. The only way to change the full_name property is to do so indirectly:

>>> person.first_name = "Dan"
>>> person.full_name
'Dan Driscoll'

This is kind of limiting, so let's look at another example where we can make a property that does allow us to set it.

Replacing Setters and Getters with a Python property

Let's pretend that we have some legacy code that someone wrote who didn't understand Python very well. If you're like me, you've already seen this kind of code before:

from decimal import Decimal
    
########################################################################
class Fees(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
        
    #----------------------------------------------------------------------
    def get_fee(self):
        """
        Return the current fee
        """
        return self._fee
    
    #----------------------------------------------------------------------
    def set_fee(self, value):
        """
        Set the fee
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value

To use this class, we have to use the setters and getters that are defined:

>>> f = Fees()
>>> f.set_fee("1")
>>> f.get_fee()
Decimal('1')

If you want to add the normal dot notation access of attributes to this code without breaking all the applications that depend on this piece of code, you can change it very simply by adding a property:

from decimal import Decimal
    
########################################################################
class Fees(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
        
    #----------------------------------------------------------------------
    def get_fee(self):
        """
        Return the current fee
        """
        return self._fee
    
    #----------------------------------------------------------------------
    def set_fee(self, value):
        """
        Set the fee
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value
    
    fee = property(get_fee, set_fee)

We added one line to the end of this code. Now we can do stuff like this:

>>> f = Fees()
>>> f.set_fee("1")
>>> f.fee
Decimal('1')
>>> f.fee = "2"
>>> f.get_fee()
Decimal('2')

As you can see, when we use property in this manner, it allows the fee property to set and get the value itself without breaking the legacy code. Let's rewrite this code using the property decorator and see if we can get it to allow setting.

from decimal import Decimal
    
########################################################################
class Fees(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._fee = None
        
    #----------------------------------------------------------------------
    @property
    def fee(self):
        """
        The fee property - the getter
        """
        return self._fee
        
    #----------------------------------------------------------------------
    @fee.setter
    def fee(self, value):
        """
        The setter of the fee property
        """
        if isinstance(value, str):
            self._fee = Decimal(value)
        elif isinstance(value, Decimal):
            self._fee = value
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    f = Fees()

The code above demonstrates how to create a "setter" for the fee property. You can do this by decorating a second method that is also called fee with a decorator called @fee.setter. The setter is invoked when you do something like this:

>>> f = Fees()
>>> f.fee = "1"

If you look at the signature for property, it has fget, fset, fdel and doc as "arguments". You can create another decorated method using the same name to correspond to a delete function using @fee.deleter if you want to catch the del command against the attribute.

Wrapping Up

Now you know how to use Python properties in your own classes. Hopefully you can find even more useful ways to use them in your own code.

Additional Reading

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary