Protecting Valid Object State · practice
Compute Information with Properties
attempt.current_score() protects the score. It also makes the class slightly worse to use than the version that had the bug, and it is worth being honest about that:
print(attempt.score) # was: readable, and dangerously assignable
print(attempt.current_score()) # is: safe, and now a function call
Every program that used attempt.score has to change, and every future reader has to remember which values on this
Python offers a way to avoid paying it.
An attribute that runs code
A property is a method that callers reach without parentheses, as though it were an attribute:
attempt.score again, with no parentheses. The line @property above the method is what makes that work. Python sees the access, runs the method, and hands back what it returns.
The @ line is called a decorator. You will meet decorators as a general idea later. For now, treat @property as the way this feature is spelled.
What that bought
The interface went back to what it was, and the protection stayed:
attempt.score = 500
Assigning to a property that only defines a getter raises AttributeError. The contradiction the chapter opened with is now genuinely unavailable, rather than merely discouraged by an underscore.
More interesting: the class is now free to change its mind about _score entirely.
There is no _score attribute at all now. The score is worked out from the stored results each time it is asked for, and every program written against attempt.score keeps working without a single change. That is the payoff for having separated interface from representation in the previous lesson.
The second Attempt never stores a score. Why can attempt.score still be read?
A stored value can go stale
Computing on demand fixes a bug class that is easy to create and hard to notice. Consider a version that keeps a running total and lets responses be removed:
def undo_last(self):
self._results.pop() # the results changed
# and _score did not
With a stored _score, that method has to remember to subtract, and the day someone forgets, the object reports a score its own results do not support. With a computed property, undo_last is complete as written: the next read of score reflects reality because it is derived from reality.
The trade is real work on every read. For a sum over a handful of results that is nothing. If a computation were expensive and read constantly, storing it would be reasonable, and the class would then own the job of keeping it correct. Prefer computing until you have a measured reason not to.
When not to use a property
A property should feel like asking an object about itself. Reading it should be quick, and should not surprise anyone.
If the work is slow, if it reaches across a network, or if reading it changes something, write an ordinary method. attempt.score implies a fact about the object. attempt.recalculate_from_server() implies an operation, and hiding an operation behind attribute syntax is how people end up with a
Task
The settled Attempt already computes score and response_count from its single _results accuracy, the fraction of responses that earned points, as a number between 0.0 and 1.0.
An attempt with no responses has an accuracy of 0.0. Deciding what to do about that division is the main part of the exercise.
Run the program and compare the three facts after undo_last(). The last response was incorrect: removing it leaves the score at 2, lowers the count from 2 to 1, and raises accuracy from 0.5 to 1.0. No code in undo_last needs to update those totals separately.