Types of Inheritance
Inheritance is the ability to create a new class that includes all the aspects of one or more existing classes, while allowing for modifications. It is a feature often associated with object-oriented programming languages, enabling one to capture some relationships between objects. The big advantage of this powerful feature is it lets you add new attributes or methods to a base class, without modifying the base class itself.
Let’s again go back to our real-world example: the concept of an Orange. In describing the concept, we would probably try to answer a fairly broad set of questions, such as, What kind of thing is an Orange? What parts and properties does an Orange have? What can we do with an Orange?
Of course, an Orange is recognized by its color and shape and its other physical properties. We can eat it and squeeze it. However, we also know that an Orange is related to other concepts: it is a kind of Fruit, and it has typical fruity components like Pulp, Seeds, and Juice.
CONCEPT | KIND | COMPONENTS | PROPERTIES | ACTIONS |
Orange | Fruit | Pulp, Seeds, Juice | color, shape | eat, squeeze |
The first two columns of our table show that the concept of an Orange relates to other concepts in two distinct ways: it is a Fruit, while it has Pulp, Seeds, and Juice.
In OOP, classes also relate to each other in two major ways. In inheritance, a child class “inherits from” a parent class, while adding details in some way. Thus, Orange is logically a subclass or subtype of Fruit; Fruit is the base class or supertype. This is also called an “is-a” relationship; an Orange is a Fruit.
In composition, though, a class merely aggregates (or is composed of) the different aspects of an object from its “parent” class(es). This is called a “has-a” relationship. As we shall see, it is not necessary to use inheritance to build such “has-a” relationships into a class.
Composition vs. inheritance
Inheritance is useful, but at times it is better just to express explicitly all the attributes and methods of the base class(es). With composition, the idea is to re-implement the interfaces that are defined for the base class(es) without necessarily re-using the original implementations (code). Why? Regular or implicit inheritance tends to cover up the details of the behavior, because all the code is hidden away in the parent class(es).
In our above example, Pulp, Seeds, and Juice could be defined as classes in their own right. Moreover, these classes could all be inherited by Fruit, which would in turn be inherited by Orange, like this:
We particularly see the potential for trouble in the first level of inheritance, which brings in three base classes at once. This is called multiple inheritance. The problem is, what happens if both Pulp and Juice have a color attribute? Which one takes precedence? It can get complicated.
It may be cleaner to allow Orange to inherit from Fruit, but to have the Fruit and Orange classes implement the Pulp, Seeds, and Juice interfaces (i.e., the attributes and methods) separately and consistently
Example of composition
Let’s return to our earlier example with the Orange class and the MultiOrange subclass to see how composition works. The two attributes of Orange—color and shape—are both “has-a” properties. This is true for MultiOrange as well as Orange, and methods in both classes affect these attributes in the same way. The output of the eat() method was overridden in MultiOrange with no real difficulty in our previous model of inheritance. So we might just as well compose MultiOrange from the individual properties of Orange, simply by referring to the attributes and methods of Orange, while defining special __init__() and eat() methods, like this:
In most respects, MultiOrange is now merely a thin wrapper over the methods and attributes defined in Orange, all of which are actually duplicated one layer down, in MultiOrange.orange. We even have to be careful that the squeeze() method updates self.shape as well as self.orange.shape. So it’s not yet clear that we’ve gained much of anything. In truth, our code becomes much more transparent if we just re-implement everything directly:
Here, we see that Orange and MultiOrange remain compatible classes, in that Orange’s methods and attributes all work on MultiOrange objects. But now MultiOrange’s code can be read and understood in its own right, without reference to Orange. We see too that at this point we might want to dispense with separate Orange and MultiOrange classes altogether, and opt instead to have one unified class Oranges, defined as above. Our previous Orange class becomes a special case of Oranges with Oranges.number=1.
In the above example, perhaps a different way of looking at the “has-a” relationship is that MultiOrange should literally consist of multiple oranges, i.e., a list of instances of type Orange. The __init__(self,num) definition would then build a list of length num. Conceptually, this scheme may be more in line with our intuition, but the implementation would undoubtedly be wasteful and slow for num=100 or num=1000.
Function defined outside of a class
You can also define a function outside of a class and use it in the class body as a method. This may improve the overall reusability of the code. In order to declare a function outside of a class, you may want to include the self parameter, as in the following example:
This will give you the following output:
Alternatively, we can do it this equivalent way, so that the self argument appears where it normally goes:
The second set of definitions gives the same behavior as before.