Hello, and welcome to the fourth episode of the Software Carpentry lecture on object-oriented programming. In this episode, we'll show you how to create new classes from old ones.
Let's take another look at the example from our previous episode. We were given a signal that has been sampled at irregular intervals…
…and we built classes to do stepwise and linear interpolation in a way that let us use one or the other—or yet another class that we haven't been written yet—in the rest of our program.
A lot of how we implemented those two classes was the same.
How can we eliminate the redundancy, so that there's no duplicated code in our program?
Here's the implementation of StepSignal's 'get' method again. It searches through the (x, y) pairs saved in self.values to find the sample immediately preceding 'where', and returns the corresponding y value. If 'where' lies outside the sampling range, 'get' raises an IndexError exception instead of returning a value.
The implementation in LinearSignal is almost identical.
In fact, only the line that actually does the interpolation is different; everything else is identical. And as we've said before, code that's duplicated will eventually be wrong.
What we want to do is refactor this code so that our methods look like this. The new method, 'find', will either return the index of the sample immediately preceding 'where', or raise an exception if 'where' is out of range. Once we have that index, we can either return the corresponding 'y' value (if we're doing stepwise interpolation) or find a weighted average (if we're doing linear interpolation).
But where should the 'find' method go?
We don't want to duplicate it in the two classes—that wouldn't really count as solving our problem.
Instead, we're going to use inheritance: we're going to create a new class that inherits all the properties of an existing one, then specialize it.
To see how this works, let's create a class called Parent that has one method, hello. As before, we put the name of Python's built-in class 'object' in parentheses after the name of the new class—we'll show you why in just a moment.
Then let's create a second class, Child, that defines another method 'goodbye'.
This time, though, we put the name of the class Parent in parentheses when we're defining Child, rather than the name 'object'. This tells Python that we want Child to have everything that we defined for Parent, as well as anything new we define specifically for Child.
To see what this means, let's create an object of class Child. As expected, we can then call its 'goodbye' method.
But look: we can also call its 'hello' method, even though we didn't define 'hello' for the class Child. This works because Child inherited the definition of 'hello' from Parent: it automatically has everything that Parent defined.
Inheritance only works in one direction, though. If we create an object of class Parent, we can call its 'hello' method, as shown here.
But if we try to call its 'goodbye' method, the call fails, because we didn't define 'goodbye' for Parent, and Parent didn't inherit it from anywhere.
This picture shows you what's going on inside Python in this example.
At the top is the class 'object', which is built into Python.
Below that is our class Parent. It inherits stuff from 'object' — that's why we've been putting object's name in parentheses every time we've defined a class so far — and adds a method of its own called 'hello'.
Below that is the class 'Child'. It inherits from Parent, and adds another method 'goodbye'.
When we create the object 'c' of class 'Child', Python puts a reference in the object to its class. When we call 'c.goodbye', Python follows that reference from 'c' to 'Child', finds the method, and executes it. When we call 'c.hello', Python follows the reference to Child, fails to find 'hello', but then sees that 'Child' has a reference to another class 'Parent'. When Python follows that link, it finds that Parent does have the method 'hello', so everything's OK.
Similarly, when Python creates the object 'p', and then calls 'p.hello', it follows p's link to Parent and finds the method. When we try to call 'p.goodbye', though, the interpreter looks in Parent, fails to find anything called 'goodbye', then follows the link up to 'object', fails again, and raises an exception. There is no reference from Parent to Child, only one from Child to Parent, so there's no way to get to 'goodbye' from the object 'p'.
All right, let's try to apply this idea to our signal interpolators. Here's a class called 'InterpolatedSignal' that has just one method called 'find'. Given an 'x' value 'where', this method searches through 'self.values' to find the location of the immediately preceding sample and returns the corresponding index. If 'where' is out of bounds, 'find' raises an exception.
This method isn't particularly useful on its own, but it is exactly what StepSignal and LinearSignal need.
Before we show how they use it, though, there's a design flaw in this class. 'find' depends on 'self.values', but InterpolatedSignal doesn't create this anywhere—it seems to just appear by magic. We'll come back and fix this in a few medias.
Now that we have a way to find where we're supposed to interpolate, let's rewrite our actual interpolating classes.
Here's the new version of StepSignal.
It's derived from InterpolatedSignal—again, that's what it means to put InterpolatedSignal's name in parentheses in the class definition.
which means it can use the 'find' method without re-defining it.
In order for that to work, though, StepSignal's constructor has to create self.values, so that it will be there when self.find needs it.
This is fragile: there's nothing in 'StepSignal' to tell the next person reading this code why we're doing this, just as there was nothing in 'InterpolatedSignal' to tell someone reading it where 'self.values' came from.
Dependencies between classes should be more explicit—let's see how to make them so.
In this case, the right solution is to have the parent class store the values we're using for interpolation.
This version of 'InterpolatedSignal' does exactly that: its constructor makes a copy of 'values' and assigns it to 'self.values'.
It also defines a method called 'get' that raised a NotImplementedError exception, so that if anyone ever tries to create an object of this class and use it for interpolation, they'll get a meaningful error message instead of Python's default "attribute not found".
We can now rewrite StepSignal's constructor as shown here.
Instead of storing 'values' itself, this constructor calls InterpolatedSignal's constructor, passing in the object being built (that's 'self') and the input parameter 'values'. The syntax 'InterpolatedSignal.__init__' is a bit clumsy, but the effect is pretty simple: StepSignal is asking InterpolatedSignal to do whatever it thinks it has to when it's creating a new object.
And here's the new LinearSignal. Its constructor is the same—it just asked the parent class to do whatever it needs to do. The beauty of this is that there's now exactly one place to make a change when we need to. For example, if we want to change the kind of exception that's raised when someone tries to get a value for a point that's out of bounds, we change InterpolatedSignal.find and we're done.
Similarly, if we want to check that the samples are actually pairs, and in order (which we should have been doing all along), we can add that code to the parent class, and both of the child classes will automatically get it.
One other thing we've done here deserves mention. As this diagram shows, we have defined a method called 'get' in two places: once in the parent class 'InterpolatedSignal, and once in the child class 'StepSignal'. The one in the parent raises an exception every time it's called, but that's OK: if we start with an object 'signal' of class 'StepSignal', and follow the references upward, we find StepSignal's 'get' first and call that, just as we want to. When this happens, we say that the child class is overriding the method in the parent class: it's defining a method with the same name which takes precedence over the parent's.
Let's have a closer look at how overriding works.
Here's a class called Parent that defines two methods, hello and goodbye.
And here's a class Child that inherits from Parent and defines its own 'goodbye' method.
That method overrides the one defined in Parent because it has the same name.
Now let's create an object of class Parent and call its methods. Good—that works as expected.
When we create an object of class Child, and call 'hello', that also does what we expect—Child didn't define 'hello', so Python looks upward to its parent, finds one there, and uses it.
But when we call the Child object's 'goodbye', we get the one defined in Child. The one in the Parent class is still there, but Python finds the one in Child first and uses it.