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
set construct. In Python, it is done using
In this article I'll be describing they Python property decorator, which you may have seen being used with the
class Person(object): def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name def full_name(self): return self.first_name + ' ' + self.last_name def full_name(self, value): first_name, last_name = value.split(' ') self.first_name = first_name self.last_name = last_name def full_name(self): del self.first_name del self.last_name
This is Python's way of creating getters, setters, and deleters (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 getter/setter/deleter 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.
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:
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,
property object acts as a descriptor, which means it has its own __get__(), __set__() and __delete__() methods. The
__set__() methods are triggered on an object when a property is retrieved or set, and
__delete__() is triggered when a property is deleted with
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
class Person: pass
property, these methods now correspond to our
full_name_setter methods from above:
full_name.fget is full_name_getter # True full_name.fset is full_name_setter # True
fset are now wrapped by
And finally, these descriptor objects can be accessed by passing a reference to our class,
'Billy', 'Bob') full_name.__get__(person) Billy Bob >>> full_name.__set__(person, 'Timmy Thomas') >>> person.first_name Timmy person.last_name Thomasperson = Person(
This is essentially how properties work under the surface.