Encapsulation with the @property decorator

When we discussed encapsulation in classes, we saw that a class’s data fields should be kept as private fields, with getter and setter methods being provided to access or change them. We saw one way of implementing encapsulation by writing explicit class methods for reading and writing a class variable. There is a somewhat cleaner method of doing this in Python, by the use of a decorator.

We’ll study decorators in more detail in another post. For now, all you need to know that a decorator provides a way of adding some extra functionality to a function, class or class method. It’s possible to write your own decorators (which we’ll get to later), however, there are several built-in decorators that you can use directly. One of these is the property decorator, which facilitates encapsulation within a class.

As usual, it’s easiest to see how a property decorator works by looking at a specific example. Here’s a Person class which stores the first, middle and last names of a person.

class Person:
    def __init__(self, nf = 'Glenn', nl = 'Rowe', nm = 'William'): 
        self.__first = nf
        self.__middle = nm
        self.__last = nl

    @property
    def firstName(self):
        return self.__first

    @firstName.setter
    def firstName(self, fName):
        self.__first = fName

    @firstName.deleter
    def firstName(self):
        del self.__first

The class’s constructor creates three private data fields named __first, __middle and __last. Recall that the double underscore at the beginning of a variable’s name indicates that it’s a private data field, and is not directly accessible by code written outside the class.

To implement encapsulation, we need to provide a getter and setter method for each private variable. In this example, I’ve done this for only one of the variables; the procedure for the others works the same way.

On line 7, we begin the definition of a getter for the __first variable by writing @property. The @ symbol is a prefix that indicates that the name that follows is a decorator. In this case, @property is a built-in decorator that provides 3 options: getter, setter and deleter.

Using @property on its own indicates that the method that follows is to be treated as a getter method. In our case, we want a getter for the person’s first name, so the method just returns self.__first. Note that the name of the getter method firstName() isn’t necessarily the same as the name of the data field that it returns (although it can be if you like). In our case, since we’ve used the double underscore naming convention to indicate that __first is a private variable, it wouldn’t make sense to use a double underscore to name a getter method. We’ll see how to call this getter method in a minute.

Next, we declare a setter property on line 11. Note that the decorator in this case is given as @firstName.setter, and not @property.setter. Defining a setter method requires that a getter method of the same name has been previously defined. The @property definition on line 7 effectively wraps tha firstName() method in other code that defines it to be a property, so we can now use its name to define a setter method. The setter takes the usual self argument, followed by another argument fName that is assigned to the private data field __first. In order to be used as a setter, a setter method must have exactly 2 arguments: self and another argument to be used in the assignment.

Finally, we can define a deleter property as on line 15. A deleter is called when a data field is to be deleted from an object.

Now we can have a look at some code that uses these properties. We have

from Person import *

me = Person()
print(me.firstName)
me.firstName = input('Enter new first name: ')
print(me.firstName)
del me.firstName
print(me.firstName)
me.firstName = 'Alex'
print(me.firstName)

Line 3 creates a Person with the default names of Glenn, William and Rowe. Line 4 illustrates how the getter can be called. Since firstName was declared as a getter property above, we can use its name (without any parentheses after it) to call the getter method. That is, the expression me.firstName calls the firstName() method in the Person class, which returns the value of __first.

Next, on line 5 we ask the user to enter a new first name. We have used an assignment (= operator) to call the setter method defined above. If a class method has been defined as a setter, the value on the right side of the = is sent to the method as its second argument. Again, all this happens without the need to explicitly call a method with parentheses. We print out me.firstName (calling the getter again) on line 6 to verify that the new first name has been allocated correctly.

On line 7 we use the del keyword to call the deleter property method above. This deletes the __first data field from the me object. When we attempt to call the getter (line 8) to print out the __first field, we get an AttributeError, since there is now no such data field.

If we remove line 8 and proceed to line 9, we are calling the setter just after calling the deleter. The setter restores the __first data field and assigns it the value ‘Alex’. Since the me object now has a __first data field again, we can access it via the getter on line 10 and print it out.

An important point should be made here. Although the @property decorator allows us to write getters, setters and deleters, it’s up to the programmer to ensure that the code within these methods actually does get, set or delete the corresponding data fields. In practice, we can write anything inside any of these methods, so there is no guarantee that the code will actually do what you’d expect, if the coder has an evil sense of humour. We could write a deleter that doesn’t actually delete anything, but instead prints out the daily weather forecast, or something equally irrelevant.

The key points are:

  1. If a method x is defined as a getter, then it can be called from an object y with the syntax y.x (without parentheses).
  2. If a method w is defined as a setter, then it will be called from an object z with a statement such as z.w = b. The b is passed in as the second argument to the setter.
  3. If a method d is defined as a deleter then for an object e it is called with the statement del e.d (again, without any parentheses).

Any code within the methods x, w and d is run in response to these statements.

The @property decorator can be used to define properties in addition to the bare data fields of a class. For example, suppose we extend the Person class above so it now looks like this, with a couple of new methods added at line 19:

class Person:
    def __init__(self, nf = 'Glenn', nl = 'Rowe', nm = 'William'): 
        self.__first = nf
        self.__middle = nm
        self.__last = nl

    @property
    def firstName(self):
        return self.__first

    @firstName.setter
    def firstName(self, fName):
        self.__first = fName

    @firstName.deleter
    def firstName(self):
        del self.__first

    @property
    def fullName(self):
        return " ".join([self.__first, self.__middle, self.__last])

    @fullName.setter
    def fullName(self, fName):
        fName = fName.split()
        self.__last = fName[2]
        self.__middle = fName[1]
        self.__first = fName[0]

We’ve defined a method fullName() which is declared to be a getter. It returns a string composed of all three parts of the person’s name. I’ve used the join() method from the string class, which has the format <str>.join(<list>), and returns a single string consisting of the elements of the list joined together with the <str> that calls join(). In this case, it produces a single string with the 3 names separated by blanks.

On line 23, I’ve added a setter for fullName, which takes in a single string consisting of 3 names separated by blanks, calls split() to separate the names, and then assigns each name to a data field in the object.

We can use the getter and setter as in the following code:

from Person import *

me = Person()
print(me.fullName)

while True:
    try:
        fName = input('Enter first middle last: ')
        me.fullName = fName
        print(me.fullName)
    except IndexError:
        print('Full name must have 3 components.')
    else:
        break

We create a Person with the default names on line 3, and call the fullName getter on line 4 to print out the name as a single string.

Lines 6 through 14 ask the user to enter a new full name. I’ve included a bit of error checking to cope with the case where the user enters fewer than 3 names in the string. On line 9 we call the fullName setter with fName passed in as the second argument. This string is split, and if there are fewer than 3 elements in the resulting list, an IndexError will be raised, which is caught on line 11. Control then passes back to the while statement on line 6 so the user can try again. If the new name is assigned correctly, control passes to the break statement on line 14 and the program exits.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.