Protecting Valid Object State · practice
Public and Internal Attributes
Attempt.record now owns the rule that turns a response into points. The contradiction from the previous lesson is still available, though:
attempt.score = 500
Nothing about the class says that score is its own business. Every attribute looks equally public, because every attribute is equally public.
A mark that means “this is mine”
Python’s convention is a single leading underscore:
learner has no underscore: it is part of what an Attempt offers the outside world. _responses, _score, and _finished do: they are how this class happens to keep track, and the class reserves the right to change them.
Be honest about what this does
The last line of that example printed 2. Read it again.
attempt._score worked. The underscore did not create an enforced access restriction for this ordinary attribute. Nothing was blocked, and nothing was reported.
Still allowed. Anyone can still do this, and the language will not
So what did the underscore accomplish? It changed the meaning of that line for a human reader. Before, attempt.score = 500 looked like using the class. Now, attempt._score = 500 looks like reaching past it. A reviewer notices. The author of Attempt has said, “this is not part of the supported interface, and I may change it tomorrow.”
That is genuinely useful, and it is weaker than a guarantee. Treat the underscore as a sign on a door, not a lock. Python also has features such as double-underscore name mangling, but a single leading underscore is a convention rather than an access modifier.
What happens when code outside the class runs attempt._score = 500?
What stays public
Marking things internal is only half the job. If _score is internal, the outside world still needs some honest way to ask how the attempt is going, or the class is useless.
That means deciding, deliberately, what the class promises:
learner, readable, because that is the attempt’s identity;record(question, response), because that is the event the object exists to absorb;something that reports the score, because reporting it was never the problem. Assigning it was.
The distinction worth carrying forward is between an object’s interface (what it promises to callers) and its representation (how it currently stores things). _score is representation. A
For now, an ordinary method is enough:
def current_score(self):
return self._score
Not elegant, and you would be right to find attempt.current_score() a clumsy way to ask for a number that used to be plain attempt.score. Lesson 5 fixes the clumsiness without giving up the protection.
Task
Decide which parts of Attempt are its interface and which are its representation, then mark them.
Rename responses and score so their names begin with a single underscore, and update every use inside the class. Leave learner public: callers are entitled to that. The _finished lifecycle state and finish()
Then add the two public methods the class needs to remain useful: current_score(), returning the score, and response_count(), returning how many responses have been recorded. Neither should let a caller change anything.
Run the program. The output should be the same as before the rename, which is the point: the interface held while the representation changed.