Problem overview
A thermostat keeps a target temperature that must always stay between 10 and 30 degrees. If any code could set the target directly, nothing would stop it from storing 95. So the target is kept inside the object, and only the thermostat's own methods change it. Every one of them keeps the target in range.
Thermostat()starts with a target of 20.set_target(t)sets the target tot, but a value below 10 becomes 10 and a value above 30 becomes 30.warmer()raises the target by 1 andcooler()lowers it by 1, never past 30 or below 10.get_target()returns the target.
For a new thermostat, two warmer() calls give 22 and a cooler() gives 21. After set_target(35) the target is 30, a warmer() leaves it at 30, and cooler() brings it to 29.
Approach: one place for the rule
The 10-to-30 rule is needed in three methods. Instead of writing it three times, write it once in set_target and let warmer and cooler go through it: warmer calls self.set_target(self._target + 1). At 30 that asks for 31, and set_target turns it back into 30. At 10, cooler asks for 9 and gets 10. If the range ever changes, there is a single place to edit.
The field is named _target. In Python a leading underscore is a convention that means "private: use the methods, not this attribute". The language doesn't enforce it, so code outside the class can still write t._target = 95, and get_target() would then return 95. The convention is a promise between programmers, and a good class keeps it by offering methods for everything outside code needs.
Step by step
- Store 20 in
self._targetin__init__. - In
set_target, handle a value below 10: anifwhose body sets the target to 10. A value that is too low isn't refused. It becomes the lowest target allowed. - Handle a value above 30 with an
elifthat sets the target to 30. - Handle everything else with an
elsethat sets the target tot. The values 10 and 30 themselves are allowed, so they end up here and are kept. warmercallsset_targetwithself._target + 1.coolercallsset_targetwithself._target - 1.get_targetreturnsself._target. Nothing can change the target through it.
The solution in Python
class Thermostat:
def __init__(self):
self._target = 20
def set_target(self, t):
if t < 10:
self._target = 10
elif t > 30:
self._target = 30
else:
self._target = t
def warmer(self):
self.set_target(self._target + 1)
def cooler(self):
self.set_target(self._target - 1)
def get_target(self):
return self._target
A run:
t = Thermostat()
print(t.get_target())
t.warmer()
t.warmer()
print(t.get_target())
t.cooler()
print(t.get_target())
t = Thermostat()
t.set_target(4)
print(t.get_target())
t.cooler()
print(t.get_target())
t.set_target(10)
t.warmer()
print(t.get_target())
20
22
21
10
10
11
Why go through the methods
The point of the exercise is that the rule lives in the class. A user of Thermostat never needs to check the range: whatever they call, the target stays valid, so every other piece of code can rely on it. That is the idea behind keeping a field private and exposing methods. The class guards its own data.
A trace of warmer() at the top of the range shows the chain. The target is 30, so warmer calls set_target(31). Since 31 is above 30, the elif branch stores 30, and the target is unchanged. Nothing in warmer itself needed to know about the limit.
Another way to clamp
The three-branch if is the clearest form for a beginner. The same rule fits in one line with min and max: self._target = max(10, min(30, t)). The inner min caps the value at 30 and the outer max lifts it to at least 10. For the inputs 4, 10, 20, 30 and 35 it gives 10, 10, 20, 30 and 30, the same as the branches. Use whichever reads better to you, as long as the rule stays in one place.
What "private" means in Python
Many languages have a private keyword that the compiler enforces. Python doesn't. The underscore in _target tells other programmers to stay out, and that is all. The attribute is a normal one: after t = Thermostat(), hasattr(t, "_target") is True, while hasattr(t, "target") is False, so the name with the underscore is the real name. Keeping to the convention is what makes the class safe in practice: the tests use only the four methods.
Edge cases and common mistakes
- Exactly 10 or 30. Both are allowed, so they must reach the
elsebranch. The tests aret < 10andt > 30, strict on purpose. - Refusing instead of clamping.
set_target(35)doesn't leave the old target in place. It sets 30. - Duplicating the rule in
warmerandcooler. It works, but the three copies can drift apart. Going throughset_targetkeeps one. - Writing to
self._targetfromwarmerdirectly asself._target += 1skips the check, and the target would reach 31. - Far-out values.
set_target(-1000)gives 10 andset_target(1000)gives 30, so the input can be anything. - Complexity. Each method does a few comparisons, so each is O(1).
Testing
- A new thermostat:
get_target()returns 20. set_target(30), thenwarmer(): the target stays 30.set_target(10), thencooler(): the target stays 10.set_target(35), thencooler(): the target is 29.
