When binding attributes, if we expose the attributes directly, although it is simple to write, there is no way to check the parameters, allowing scores to be modified arbitrarily:
s = Student()
s.score = 9999
This is obviously illogical. To restrict the range of score, we can set the score through a set_score() method and retrieve it through a get_score() method. This way, we can validate the parameters inside the set_score() method:
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must be between 0 and 100!')
self._score = value
Now, when operating on any Student instance, we can no longer set score arbitrarily:
>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
...
ValueError: score must be between 0 and 100!
However, the above calling method is somewhat cumbersome and not as straightforward as using attributes directly.
Is there a way to both validate parameters and access class variables in a simple, attribute-like manner? For perfection-seeking Python programmers, this is a must-have feature!
Do you remember that decorators can dynamically add functionality to functions? Decorators work equally well for class methods. Python’s built-in @property decorator is responsible for turning a method into an attribute access:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must be between 0 and 100!')
self._score = value
The implementation of @property is relatively complex, so let’s first look at how to use it. To turn a getter method into an attribute, simply add @property to it. At this point, @property itself creates another decorator @score.setter, which is responsible for turning a setter method into attribute assignment. Thus, we have controllable attribute operations:
>>> s = Student()
>>> s.score = 60 # OK, actually converted to s.score(60)
>>> s.score # OK, actually converted to s.score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must be between 0 and 100!
Notice this magical @property: when we operate on instance attributes, we know that the attribute is most likely not directly exposed, but implemented through getter and setter methods.
We can also define read-only attributes by only defining the getter method (omitting the setter method):
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2015 - self._birth
In the code above, birth is a read-write attribute, while age is a read-only attribute because age can be calculated from birth and the current year.
Important Note: The name of the attribute method should not conflict with the instance variable name. For example, the following code is incorrect:
class Student(object):
# Both method name and instance variable are 'birth':
@property
def birth(self):
return self.birth
This is because when s.birth is called, it is first converted to a method call. When executing return self.birth, it is treated as accessing the self attribute, which in turn is converted to a method call self.birth(), causing infinite recursion and ultimately a stack overflow error (RecursionError).
If the attribute method name conflicts with the instance variable name, it will cause recursive calls and result in a stack overflow error!
@property is widely used in class definitions. It allows callers to write concise code while ensuring necessary parameter validation, thereby reducing the likelihood of errors during program execution.
Use @property to add width and height attributes to a Screen object, as well as a read-only resolution attribute:
class Screen(object):
pass
# Test:
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
print('Test passed!')
else:
print('Test failed!')
use_property.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError("score must be an integer!")
if value < 0 or value > 100:
raise ValueError("score must between 0 ~ 100!")
self._score = value
s = Student()
s.score = 60
print("s.score =", s.score)
# ValueError: score must between 0 ~ 100!
s.score = 9999