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
__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.
@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
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:
- If a method
xis defined as a getter, then it can be called from an object
ywith the syntax
- If a method
wis defined as a setter, then it will be called from an object
zwith a statement such as
z.w = b. The
bis passed in as the second argument to the setter.
- If a method
dis defined as a deleter then for an object
eit is called with the statement
del e.d(again, without any parentheses).
Any code within the methods
d is run in response to these statements.
@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 self.__middle = fName self.__first = fName
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.