Appearance
Using @property
When binding attributes, if we expose the attribute directly, it might be simple to write, but we cannot validate the parameters, which allows arbitrary modification of values. For example:
python
s = Student()
s.score = 9999
This is clearly illogical. To restrict the range of score
, we can use a set_score()
method to set the score and a get_score()
method to retrieve it. This way, we can validate the parameters within the set_score()
method:
python
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 ~ 100!')
self._score = value
Now, when operating on any Student
instance, you cannot set score
arbitrarily:
python
>>> 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 ~ 100!
However, the above method calls are 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 perfectionist Python programmers, this is a necessity!
Remember that decorators can dynamically add functionality to functions? The same applies to class methods. Python's built-in @property
decorator is responsible for turning a method into an attribute:
python
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 ~ 100!')
self._score = value
The implementation of @property
is quite complex, so let's first examine how to use it. To turn a getter method into an attribute, simply add @property
. At this point, @property
itself creates another decorator @score.setter
, which turns a setter method into an attribute assignment. This way, we have a controlled attribute operation:
python
>>> s = Student()
>>> s.score = 60 # OK, actually converted to s.set_score(60)
>>> s.score # OK, actually converted to s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must be between 0 ~ 100!
Notice that with this magical @property
, when we operate on the instance's attribute, the attribute is likely not directly exposed but implemented through getter and setter methods.
You can also define read-only attributes by only defining the getter method without a setter method:
python
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 above example, birth
is a read-write attribute, while age
is a read-only attribute because it is calculated based on birth
and the current time.
Important Note: Do not name property methods the same as instance variables. For example, the following code is incorrect:
python
class Student(object):
# The method name and instance variable are both 'birth':
@property
def birth(self):
return self.birth
This is because when calling s.birth
, it first converts to a method call. When executing return self.birth
, it again tries to access self
's attribute, which converts back to a method call self.birth()
, causing infinite recursion and ultimately leading to a stack overflow with a RecursionError
.
Caution
Naming property methods the same as instance variables can cause recursive calls, leading to a stack overflow and a RecursionError
.
Summary
The @property
decorator is widely used in class definitions. It allows callers to write concise code while ensuring that necessary parameter validations are performed, thereby reducing the likelihood of runtime errors.
Exercise
Use @property
to add width
and height
attributes to a Screen
object, as well as a read-only resolution
attribute:
python
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!')