0%

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:

Try it

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.

Try it

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 that returns the score is interface. Keeping them apart is what lets you change your mind about the second without breaking every program that uses the first, which is exactly what the next lesson does.

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() from the previous lesson are already carried forward.

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.