How can you customize what happens when you assign to a specific attribute on a Python class?
Here we have a class called Person:
class Person:
def __init__(self, name, location):
self.name = name
self.location = location
Person objects have a name attribute and a location attribute:
>>> trey = Person("Trey", "San Diego")
>>> trey.name
'Trey'
>>> trey.location
'San Diego'
We want to make it so that whenever we assign to the location attribute all previous values of the location attribute would be stored somewhere.
>>> trey.location = "Portland"
We'd like to make a past_locations attribute which would show us all previous values for location on a specific Person object:
>>> trey.past_locations
In many programming languages, like Java, the solution to this problem is getter methods and setter methods.
So instead of having a location attribute, we would have a private attribute.
Python doesn't have private attributes, but sometimes we put an underscore (_) before an attribute name to note that it's private by convention.
Here's an updated version of our Person class with a getter and setter method and a private (by convention) _location attribute:
class Person:
def __init__(self, name, location):
self.name = name
self.past_locations = []
self.set_location(location)
def get_location(self):
return self._location
def set_location(self, location):
self._location = location
self.past_locations.append(location)
On this new class, instead of accessing location directly (which doesn't work anymore) we would call the get_location method:
>>> trey = Person("Trey", "San Diego")
>>> trey.get_location()
'San Diego'
And to set a location we'd call the set_location method:
>>> trey.set_location("Portland")
>>> trey.get_location()
'Portland'
The benefit of getter and setter methods is that we can hook into these methods, putting any code we'd like inside them.
For example in our set_location method, we're appending to the past_locations list:
def set_location(self, location):
self._location = location
self.past_locations.append(location)
So that past_locations attribute now shows all locations we've ever set for this Person object:
>>> trey.past_locations
['San Diego', 'Portland']
In Python, we don't tend to use getter and setter methods.
We don't have a get_name, and a set_name, and a get_location, and a set_location for every single attribute.
Instead, we tend to just assign the attributes and read from attributes as we like.
But in this particular situation (where we have an attribute and we'd like to change how it works) we're kind of stuck. We need some way to hook into the assignment of that attribute. Fortunately, in Python, there is a way to do this: we can use a property.
Here's our modified Person class with properties:
class Person:
def __init__(self, name, location):
self.name = name
self.past_locations = []
self.location = location
@property
def location(self):
return self._location
@location.setter
def location(self, location):
self._location = location
self.past_locations.append(location)
Properties allow us to hook into the getting of an attribute.
>>> trey = Person("Trey", "San Diego")
>>> trey.location
'San Diego'
When we access the location attribute now, under the hood it's actually accessing the _location attribute.
But properties also allow us to customize what happens when we assign to a specific attribute.
By default, if we assign to a property we'll get an error. But in our case, we don't get an error:
>>> trey.location = "Portland"
>>> trey.location
'Portland'
It works because we've implemented a setter for our property:
@location.setter
def location(self, location):
self._location = location
self.past_locations.append(location)
The syntax for property setters is a little bit weird, so I don't recommend memorizing it (just look it up when/if you need it).
The property setter syntax starts with the name or our property (location) followed by .setter.
We use that as a decorator to decorate a location method (named the same as our property):
@location.setter
def location(self, location):
...
We accept an argument which represents whatever is assigned to this property (on the right-hand side of the equals sign during assignment).
We're then storing that actual value on _location, and every time our location changes, we're appending each value to our past_location list:
self._location = location
self.past_locations.append(location)
So when we access the past_locations list, both of our locations are reflected:
>>> trey.past_locations
['San Diego', 'Portland']
Any time the location changes, past_locations will be updated.
Notice that even in our initializer, we're assigning to location (rather than _location):
def __init__(self, name, location):
self.name = name
self.past_locations = []
self.location = location
That's the reason "San Diego" is the first value in this past_locations list: when we assigned to the location attribute in our initializer it called our property setter.
In fact, every time location is assigned to, the setter will be called for this property.
If you would like to customize what happens when you assign to a specific attribute on your class in Python, you can use a property with a setter.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.