Property decorator
Now that you hopefully understand that decorators are, let us make sense of some of the decorators provided by Python.
The main one that you might have encountered is the @property
decorator.
I first introduced this back in Week 1 when I discussed encapsulation in OOP. Let us revisit this now that you have had more experience with OOP, and also understand what decorators are.
As a recap, there are times in OOP when you do not want to expose your attributes directly. For example, you might not want someone else to modify your attributes directly (maybe you want to ensure that the values are legal). Or you want to embellish your attributes before allowing someone to access it (i.e. putting on some makeup). Sort of like you putting on a face mask before going out, in case someone passes a certain virus to you!
For example, let’s say we have instances of VainPerson
who wish to always be addressed by a long title. And the name must always be stored in uppercase, and the VainPerson
will only allow their name to be changed if they like the sound of the new name. We can implement this by keeping the __name
attribute private (double underscores indicate that a variable is private), and using getter and setter methods to access __name
in a controlled manner (this is the way you would do it in Java).
class VainPerson:
def __init__(self, name):
self.set_name(name)
def get_name(self):
return f"Your Most Honourable Excellency {self.__name}"
def set_name(self, name):
if self.likes_name(name):
self.__name = name.upper()
else:
# Just so we have a default name
if not self.__name:
self.__name = "JOSIAH"
def likes_name(self, name):
if name.upper() in ["EDWARD", "VICTORIA", "WILLIAM", "GEORGE", "ELIZABETH", "JOSIAH"]:
return True
else:
return False
lovely_person = VainPerson("EDWARD")
print(lovely_person.get_name()) # Your Most Honourable Excellency EDWARD
print(lovely_person.__name) # AttributeError: 'VainPerson' object has no attribute '__name'
lovely_person.set_name("loser")
print(lovely_person.get_name()) # Your Most Honourable Excellency EDWARD
lovely_person.set_name("george")
print(lovely_person.get_name()) # Your Most Honourable Excellency GEORGE
lovely_person.__name = "Liz" # __name is private. Not set to "Liz"
print(lovely_person.get_name()) # Your Most Honourable Excellency GEORGE
Properties
In the above code, get_name()
and set_name()
are clunky hard to read. It would have been nicer if we could just simply use:
lovely_person.name = "Edward"
print(lovely_person.name)
Can we still access the name
attribute this way, but without exposing the attribute?
The answer is yes. All we need to do is to create a property object called name
, and link the property to the getter method get_name()
and the setter method set_name()
. Here is a cleaner version of the code from above.
class VainPerson:
def __init__(self, name):
self.name = name
def get_name(self):
return f"Your Most Honourable Excellency {self.__name}"
def set_name(self, name):
if self.likes_name(name):
self.__name = name.upper()
else:
# Just so we have a default name
if not self.__name:
self.__name = "JOSIAH"
def likes_name(self, name):
if name.upper() in ["EDWARD", "VICTORIA", "WILLIAM", "GEORGE", "ELIZABETH", "JOSIAH"]:
return True
else:
return False
name = property(get_name, set_name)
lovely_person = VainPerson("EDWARD")
print(lovely_person.name) # Your Most Honourable Excellency EDWARD
lovely_person.name = "loser"
print(lovely_person.name) # Your Most Honourable Excellency EDWARD
lovely_person.name = "george"
print(lovely_person.name) # Your Most Honourable Excellency GEORGE
Much nicer to read, is it not?
The property
class has a constructor, which takes a function (the getter method) as the first parameter, and optionally a setter method as the second parameter. Python will invoke the correct methods when a user accesses the .name
attribute of a VainPerson
instance.
If you omit the setter method (the second argument), then your property becomes a read-only property.
class VainPerson:
def __init__(self, name):
self.name = name
def get_name(self):
return f"Your Most Honourable Excellency {self.__name}"
name = property(get_name)
lovely_person = VainPerson("EDWARD")
print(lovely_person.name) # Your Most Honourable Excellency EDWARD
lovely_person.name = "loser" # AttributeError: can't set attribute
Property decorator
Now, there is still too much repetition there. We still have to bind the name
property to the getter method get_name
and setter method set_name
. Can we just simplify this?
Notice the line name = property(get_name)
. Does that remind you of something we talked about earlier?
Like decorators?
So let’s use decorators to make our code even more readable!
class VainPerson:
def __init__(self, name):
self.name = name
@property
def name(self):
return f"Your Most Honourable Excellency {self.__name}"
So the code above is equivalent to the one before with name = property(get_name)
. Note that the method name now has the same name as the property name (name
in our case).
To also assign a setter method to this property, we use a @property_name.setter
decorator.
class VainPerson:
def __init__(self, name):
self.name = name
@property
def name(self):
return f"Your Most Honourable Excellency {self.__name}"
@name.setter
def name(self, new_name):
self.__name = new_name.upper()
So the code above is equivalent to the one with name = property(get_name, set_name)
.
Note that the method name for the setter also has the same name as the property name. You also need to use the property name in the decorator.
Now, users of your VainPerson
class can both read and write to the name
property, and you also made your code more readble in the process!