Setters that check the value
To let the client change a property with an ordinary assignment, add a setter: a second method with the same name, with the decorator @name.setter above it. The setter gets the new value as a parameter, so it can check the value before storing it:
class Thermostat:
def __init__(self):
self.__target = 20
@property
def target(self):
return self.__target
@target.setter
def target(self, degrees):
if 5 <= degrees <= 30:
self.__target = degrees
heater = Thermostat()
heater.target = 23
print(heater.target)
heater.target = 45
print(heater.target)
23
23For the client, heater.target = 23 looks just like assigning to an attribute. Behind the scenes the setter runs, and it quietly ignores 45.
The getter has to come first. @property creates the property called target, and @target.setter then adds a setter to it. With the setter first, Python stops at the decorator with NameError: name 'target' is not defined, because no target exists yet.
Saying no out loud
Ignoring a bad value hides the mistake: the client thinks the heater is set to 45. It's usually better to raise an exception, as you did in part 6. A ValueError is the usual one for a value that isn't acceptable:
class Thermostat:
def __init__(self):
self.__target = 20
@property
def target(self):
return self.__target
@target.setter
def target(self, degrees):
if degrees < 5 or degrees > 30:
raise ValueError("The target must be from 5 to 30 degrees")
self.__target = degrees
heater = Thermostat()
heater.target = 23
print(heater.target)
heater.target = 45
23Traceback (most recent call last):
File "main.py", line 19, in <module>
heater.target = 45
^^^^^^^^^^^^^
File "main.py", line 12, in target
raise ValueError("The target must be from 5 to 30 degrees")
ValueError: The target must be from 5 to 30 degreesThe client can catch the error with try and except ValueError and decide what to do, for example ask the user again.
The setter in __init__
The value given to the constructor needs the same check. Instead of writing the check twice, let __init__ use the setter: self.target = degrees, without the underscores, assigns through the property and so runs the setter.
class Thermostat:
def __init__(self, degrees):
self.target = degrees # runs the setter, which checks the value
Now Thermostat(45) raises the same ValueError, and no thermostat can ever start with a bad target.
One trap: inside the setter, store the value in the private attribute, self.__target = degrees. Writing self.target = degrees there would call the setter again, which would call itself again, and so on, until Python stops with a RecursionError.