0%

Inheritance and Polymorphism · practice

Override and Extend Behavior

Each subclass in the last lesson supplied its own is_correct. That is overriding: a subclass defines a the base already has, and the subclass’s version is the one that runs.

There is a second thing a subclass can do, and the difference matters.

Replace, or add to

Overriding describe replaces the base version completely:

Try it

The point has disappeared from the description. The subclass rewrote the whole sentence, and every improvement made to the base version from now on will pass it by.

Often what you want is the base version plus something:

Try it

super().describe() calls the base version from inside the override, and the subclass adds to what comes back. The points reappear, and they will keep appearing if the base changes how it words them.

This is the same super() as in __init__, doing the same job: run the base’s version of this method. __init__ is simply the case you meet first.

A subclass's describe calls super().describe() and appends to the result. What does that buy over rewriting the whole string?

Where each method comes from

With a family of classes, “which version runs?” needs an answer you can rely on. Python looks at the ’s own class first, then its base, then the base’s base, and stops at the first match.

Try it

summary is defined only on Question, and it calls self.describe(). The version that runs is TextQuestion’s, because self is a TextQuestion and the search starts there.

This is worth pausing on, because it is what makes a base class useful beyond sharing code. Question.summary was written without knowing what kinds of question would ever exist, and it still calls the right describe for each one. A base class can define the shape of an operation and leave the varying part to its subclasses.

Overriding responsibly

Two rules keep an override from surprising the code that calls it.

Accept what the base accepts. If Question.is_correct(response) takes one , a subclass version must work with one argument. A subclass that requires an extra one breaks every caller that was written against the base.

Return what the base returns. If is_correct returns True or False, a subclass returning "yes", or a number of points, or None for “unanswered”, will pass through if question.is_correct(...) and behave in ways nobody predicted.

Try it

No error. Just Summary: None reaching a screen somewhere. The next lesson but one is entirely about this class of problem.

Extending state as well as behavior

__init__ follows the same pattern, and now you can see it as one case of a general rule:

def __init__(self, prompt, points, tolerance):
    super().__init__(prompt, points)     # what every question needs
    self.tolerance = tolerance           # what this kind adds

Call the base first, then add. Doing your own setup before calling super().__init__ is legal and occasionally necessary, but it means part of the object exists before the base has finished, which is the Chapter 5 problem of a half-built object, arriving through a new door.

Task

Each subclass here rewrites the whole description, so a change to the base wording would never reach any of them.

Make all three extend the base description instead of replacing it. Each describe() should call super().describe() and add its own part:

  • TextQuestion: "<base> Type your answer."

  • ChoiceQuestion: "<base> Options: A, B, C." listing its own options

  • NumericQuestion: "<base> Answer within 0.2." using its own tolerance

The base wording is "<prompt> (<points> points)", and none of the subclasses may contain that format. The grader changes the base wording and checks that all three follow, which is the whole reason to do it this way.

Then add summary() to the base only, returning "Summary: <description>". Written once, it must produce the right description for each kind.