It is often considered best practice to create getters and setters for a class's public properties. Many languages allow you to implement this in different ways, either by using a function (like person.getName()
), or by using a language-specific get
or set
construct. In Python, it is done using @property
.
In this article I'll be describing they Python property decorator, which you may have seen being used with the @decorator
syntax:
class Person(object):
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return self.first_name + ' ' + self.last_name
@full_name.setter
def full_name(self, value):
first_name, last_name = value.split(' ')
self.first_name = first_name
self.last_name = last_name
This is Python's way of creating getters and setters (or mutator methods) for a property in a class.
In this case, the @property
decorator makes it so you call the full_name(self)
method like it is just a normal property, when in reality it is actually a method that contains code to be run when the property is set.
Using a setter like this provides us with quite a few advantages, a few of which I've listed here:
- Validation: Before setting the internal property, you can validate that the provided value meets some criteria, and have it throw an error if it doesn't.
- Lazy loading: Resources can by lazily loaded to defer work until it is actually needed, saving time and resources
- Abstraction: Getters and setters allow you to abstract out the internal representation of data. Like our example above, for example, the first and last names are stored separately, but the getters and setters contain the logic that uses the first and last names to create the full name.
- Debugging: Since mutator methods can encapsulate any code, it becomes a great place for interception when debugging (or logging) your code. For example, you could log or inspect each time that a property's value is changed.
Python achieves this functionality with decorators, which are special methods used to change the behavior of another function or class. In order to describe how the @property
decorator works, let's take a look at a simpler decorator and how it works internally.
A decorator is simply a function that takes another function as an argument and adding to its behavior by wrapping it. Here is a simple example:
# decorator.py
def some_func():
print 'Hey, you guys'
def my_decorator(func):
def inner():
print 'Before func!'
func()
print 'After func!'
return inner
print 'some_func():'
some_func()
print ''
some_func_decorated = my_decorator(some_func)
print 'some_func() with decorator:'
some_func_decorated()
Running this code gives you:
$ python decorator.py
some_func():
Hey, you guys
some_func() with decorator:
Before func!
Hey, you guys
After func!
As you can see, the my_decorator()
function dynamically creates a new function to return using the input function, adding code to be executed before and after the original function runs.
The property
decorator is implemented with a pattern similar to the my_decorator
function. Using the Python @decorator
syntax, it receives the decorated function as an argument, just like in my example: some_func_decorated = my_decorator(some_func)
.
So, going back to my first example, this code:
@property
def full_name_getter(self):
return self.first_name + ' ' + self.last_name
Is roughly equivalent to this:
def full_name_getter(self):
return self.first_name + ' ' + self.last_name
full_name = property(full_name_getter)
Note that I changed some function names for clarity.
Then, later on when you want to use @full_name.setter
as we do in the example, what you're really calling is:
def full_name_setter(self, value):
first_name, last_name = value.split(' ')
self.first_name = first_name
self.last_name = last_name
full_name = property(full_name_getter)
full_name = full_name.setter(full_name_setter)
Now this new full_name
object (an instance of the property
object) has both getter and setter methods.
In order to use these with our class, Person
, the property
object acts as a descriptor, which means it has its own __get__() and __set__() methods. The __get__()
and __set__()
methods are triggered on an object when a property is retrieved or set.
So person.full_name = 'Billy Bob'
triggers the __set__()
method, which was inherited from object
. This brings us to an important point - your class must inherit from object
in order for this to work. So a class like this would not be able to use setter properties since it doesn't inherit from object
:
class Person:
pass
Thanks to property
, these methods now correspond to our full_name_getter
and full_name_setter
methods from above:
full_name.fget is full_name_getter # True
full_name.fset is full_name_setter # True
fget
and fset
are now wrapped by .__get__()
and .__set__()
, respectively.
And finally, these descriptor objects can be accessed by passing a reference to our class, Person
:
>>> person = Person('Billy', 'Bob')
>>>
>>> full_name.__get__(person)
Billy Bob
>>>
>>> full_name.__set__(person, 'Timmy Thomas')
>>>
>>> person.first_name
Timmy
>>> person.last_name
Thomas
This is essentially how properties work under the surface.