Lesson 10 - Properties in Python
In the previous lesson, Static class members in Python, we learned about static class members in Python. In today's tutorial, we're going to look at the other class members that we haven't gone over yet.
Properties
We often want to have control over how an object attribute is being changed
from outside of the class. We'd like to set the attribute as read-only or react
to its changes somehow. Let's create a new project (name it
properties
) and add the following Student
class which
will represent a student in a database:
class Student: def __init__(self, name, gender, age): self.name = name self.male = gender self.age = age self.full_aged = (age > 18) def __str__(self): i_am_full_aged = "I'm" if self.full_aged else "I'm not" gender = "male" if self.male else "female" return "I'm {0}, {1}. I'm {2} years old and {3} of age.".format( self.name, gender, self.age, i_am_full_aged)
The class is very simple, the student has a name, gender, and age. The
full_aged
attribute is set according to the age and provides a more
comfortable determination whether the student is of age from different places in
the system. We use a Boolean
value to store the gender,
True
indicates that he's male. The constructor will determine
whether the student is of age or not. The __str__()
method has been
altered to suit our needs. In a real world situation, it'd probably just return
the student's name. Let's create a student using the constructor:
s = Student("Peter Brown", True, 20) print(s) input()
The output:
Console application
I'm Peter Brown, male. I'm 20 years old and I am of age.
Everything looks nice, but the attributes can be re-written. We can break the object like this, so it'll no longer function properly (i.e. would have an inconsistent internal state):
s = Student("Peter Brown", True, 20) s.age = 15 s.male = False print(s) input()
The output:
Console application
I am Peter Brown, female. I am 15 years old and I am of age.
Certainly, we would want full_aged
to be updated when the
age
is changed. Aside from that, no other attribute would need to
be altered externally. Students don't usually change their genders or names.
However, we want to keep the properties accessible for reading, so we can't make
them private. In earlier lessons of our course, we've used get methods to read
private attributes. We would name them something like get_age()
and
so on. We'll create get methods to be able to read certain attributes and make
these attributes private to prevent them from being modified from the outside.
The class would now look something like this (I omitted the constructor and
__str__()
):
class Student: def __init__(self, name, gender, age): self.__name = name self.__male = gender self.__age = age self.__full_aged = (age > 18) def __str__(self): i_am_full_aged = "I'm" if self.__full_aged else "I'm not" gender = "male" if self.__male else "female" return "I'm {0}, {1}. I'm {2} years old and {3} of age.".format( self.__name, gender, self.__age, i_am_full_aged) def get_name(self): return self.__name def is_full_aged(self): return self.__full_aged def get_age(self): return self.__age def male(self): return self.__male def set_age(self, value): self.__age = value self.__full_aged = True if age < 18: self.__full_aged = False
The methods just returning a value are very simple. In the method setting the
age is some more logic, since we have to reconsider the full_aged
attribute. We made sure we can't set variables in any other way than what we
want. We now control all the attribute changes and can work with them as needed.
Our design must prevent all unwanted changes of the internal state that would
cause an object to malfunction.
Methods for returning values are called getters and methods
for setting values are called setters. We could potentially add
an edit_student()
method to edit the other attributes sort of like
the constructor. Essentially, the student's name, age, and other attributes
would be updated using this method. We could also validate the values being set
there since we would be able to handle all attempts to change certain values in
one place. Implementing getters and setters manually is without a doubt hard
work. Isn't there any better way? Yep, there's shorter syntax in Python for us!
In that case, we're no longer talking about attributes, rather
properties.
The property syntax is very similar to the method syntax:
@property def name(self): return self.__name # the attribute bound to the method
We create a method that has the same name as the desired property name. In
the method body, we return the attribute associated with the property. We
decorate the method with the @property
decorator. This makes the
method a property. Decorators have the following syntax:
@decorator_name
def some_method(self):
...
In fact, decorators are callable objects (for example, functions, methods, or
objects with the __call__()
method) that return a modified version
of a method or function. Therefore, the following syntax can also be used:
original_method_name = decorator_name()
In Python, we have a private attribute, and we have two methods for it that Python calls based on the context (depending on the situation, whether we're reading or writing). If we don't add a setter method to the property, it can't be changed from within nor from outside. We then use the property as it was a regular attribute:
print(my_object.name) # reading my_object.name = "John Black" # writing
If we wish the property to be writable, not just readable, we could define a setter as well. Let's demonstrate it on our example of the full-aged problem which must be re-evaluated when the student's age changes:
... @property def age(self): return self.__age @age.setter def age(self, value): self.__age = value self.__full_aged = (value > 18)
First and foremost, we'll have to create a private __age
attribute, the value will be stored there. We'll work with this attribute
in the getter and the setter. In the setter, we'll use another
parameter for the value to be assigned. We'll treat the age
as we
would treat an attribute from now. Re-assignment of the age
triggers the internal logic to update the full_aged
attribute:
s.age = 15 # the full_aged attribute will update immediately as well
Likewise, we could implement a custom getter and log something:
@property_name.getter def property_name(self): return private_property_attribute
If we wanted the getter to act differently, we would modify the method body.
However, getter always has to return something, otherwise it would be no getter
__dict__ and __slots__
When declaring properties, we can use either a private attribute (see above) to store the value into or a public attribute as well. However, if we used a public attribute, the attribute and the property method names would be the same and the program would fall into recursion. But it's possible to work this around. All object attributes are kept in the dictionary associated with the object. We'll get to it like this:
object_name.__dict__
Then we can read/assign the value without recursion:
object_name.__dict__["name_name"] # read object_name.__dict__["name_name"] = value # write
However, if there is no valid reason for the property attribute to be
public, it is unnecessary. Moreover, there is one pitfall. In this way,
the values can be changed from the outside without being checked. We can
sanitize it with __slots__
, which should not be used at all.
According to Python documentation, we should use __slots__
only if
we create a large number of instances of a class and want to save memory at all
costs.
Nevertheless, even private attributes can be accessed with the help of name mangling - we'd get the attribute using the following syntax:
my_object.__ClassName_attribute_name
But this is certainly not getting / changing a private attribute by mistake
In the next lesson, Magic Methods in Python,
we'll look at the object magic methods.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 5x (1.52 kB)
Application includes source codes in language Python