One of the cornerstones of OOP is the concept of inheritance. The idea is best illustrated with an example.
Suppose we have a number of objects that all have one or more properties in common. An example from geometry is that of the regular polygons. All such polygons have a number of sides all the same length, and they also all have an area. We can extract the properties that are common to all these polygons and encapsulate them in a class that we’ll call Polygon.
Although any given polygon will have a fixed number of sides, and a fixed length for each side, the formulas for calculating the area of each type of regular polygons depends on the type of polygon (triangle, square, pentagon, etc). We can therefore create a separate class for each type of polygon, in which the formula for the area of that type of polygon is given. The Polygon class, containing the common properties, is known as the base class, and each of the specialized classes for each type of polygon is said to inherit the base class. These specialized classes are called derived classes or subclasses of the base class.
Proper use of inheritance
It’s worth pointing out that inheritance should be used only in cases where you have a number of objects that are each a specialized type of some more general type of object, as we’ve seen here with polygons. I’ve seen some examples on the web (that shall remain nameless) that give examples of inheritance that are, frankly, just plain wrong. For example, one site gives an example where a Cube class inherits a Square class, presumably because cubes have square faces. However, a cube is not a specialized type of square; it’s a completely different type of object (3-d versus 2-d), so inheritance is not appropriate here. It makes sense for a Cube class to contain a Square object (a process known as composition) but not for a Cube class to inherit a Square class.
A test that is useful when deciding whether or not to use inheritance is to ask yourself: is one type of object a specialized type of a more general type of object? Or, to put it more succinctly, does it pass the ‘is-a-type-of’ test? If not, don’t use inheritance.
Polygon classes
It’s easiest to see how inheritance works by looking at the code for this example. Here I’m using the area formulas that we met earlier when discussion lambda functions. Here’s the code:
# In file Polygon.py from math import * class Polygon: def __init__(self, numSides, side): self.numSides = numSides self.side = side self.area = 0 def PrintSides(self): print(f'Sides = {self.numSides} of length {self.side:.4f}', end='; ') def PrintArea(self): print(f'Area = {self.area:.4f}') class Triangle(Polygon): def __init__(self, s): Polygon.__init__(self, side = s, numSides = 3) self.area = sqrt(3) * self.side ** 2 / 4 class Square(Polygon): def __init__(self, s): Polygon.__init__(self, side = s, numSides = 4) self.area = self.side ** 2 class Pentagon(Polygon): def __init__(self, s): Polygon.__init__(self, side = s, numSides = 5) self.area = sqrt(5 * (5 + 2 * sqrt(5))) * self.side ** 2 / 4
On lines 5 through 15 we define the Polygon
base class. We see that the constructor on line 6 expects the number of sides and side length of the polygon to be passed into it, but it just sets the area of the polygon to zero, since we don’t know in advance what formula to use to calculate the area. Following the constructor there are a couple of methods that just print out the values.
Line 17 shows our first example of a derived class. The syntax for inheriting a base class is class <Derived>(<Base>):
, where <Derived> is the name of the derived class, and <Base> (in parentheses) is the name of the class being inherited. As usual, we must end the line with a colon.
In this example, we provide only a constructor for Triangle
, on line 18. The constructor requires only one argument, namely the side length s. The number of sides for a triangle is, of course, 3 so we don’t need to input that. In general, a derived class can have any number of methods, just as with any other class. More on this later.
By inheriting Polygon
, Triangle
inherits all its variables and methods as well. Thus the PrintSides()
and PrintArea()
methods defined in the Polygon
class become part of Triangle
, and can be called by any Triangle
object.
The Polygon constructor is also accessible to Triangle, as is shown on line 19. We can access a method from a base class by prefixing the method’s name with the name of the base class, in this case, Polygon
. Thus the base class constructor is called, which initializes the attributes numSides
, side
and area
. After this, on line 20, we calculate the triangle’s area.
The rest of this file creates two more derived classes, one for squares and one for pentagons. The process is exactly the same as for triangles.
The super() method
There is another way of referring to a base class’s methods than that of explicitly naming the base class, as we did in the previous example. This uses the built-in super()
method.
super()
has given rise to a lot of controversy in the Python community, as its use can lead to hard-to-find bugs in more complex programs. These types of programs usually involve multiple inheritance (where a derived class inherits more than one base class). We’ll leave a discussion of these situations until later; here we’ll just illustrate how super()
can be used in the a simple case of single inheritance.
We can modify the above program to use super()
as follows:
# In file Polygon.py from math import * class Polygon: def __init__(self, numSides, side): self.numSides = numSides self.side = side self.area = 0 def PrintSides(self): print(f'Sides = {self.numSides} of length {self.side:.4f}', end='; ') def PrintArea(self): print(f'Area = {self.area:.4f}') class Triangle(Polygon): def __init__(self, s): super().__init__(side = s, numSides = 3) self.area = sqrt(3) * self.side ** 2 / 4 class Square(Polygon): def __init__(self, s): super().__init__(side = s, numSides = 4) self.area = self.side ** 2 class Pentagon(Polygon): def __init__(self, s): super().__init__(side = s, numSides = 5) self.area = sqrt(5 * (5 + 2 * sqrt(5))) * self.side ** 2 / 4
The only changes we’ve made are to replace the explicit calls to the Polygon constructor from the derived classes with calls using super()
. On line 19, for example, we make a call to super()
. What super()
does is to create and return a proxy object from the base class, so in this case, it creates a temporary object of the Polygon
class. This proxy is then used to call the constructor from the Polygon
class, and you can see on line 19 that we pass into this constructor values for side
and numSides
(by using keyword arguments). Thus the constructor in Polygon
(on line 6) performs the initialization of these two variables for the Triangle
object.
Notice that we do not pass self as an argument to the base class constructor when using super()
, as super()
implicitly passes the proxy object to the base class along with the other arguments in the constructor.
If your programs involve only single inheritance, it’s largely a matter of taste whether you use explicit naming of the base class or the super()
method when calling base class methods.
Overriding class methods
The example above in which the constructor was overridden in a derived class is just a special case of method overriding. Any class method can be overridden in a derived class just by giving the method the same name and argument list in the derived class as it has in the base class. The base class version of the method can be called using super()
, just as with the constructor. An example of this is contained in Exercise 2 below.
Exercise 1
Why does the above example of the Polygon class and its derived classes not work if we swap, for example, lines 19 and 20?
See answerAn object’s data fields exist only after they are created in the constructor. Line 20 refers to self.side
, which is created by the call to the base class (Polygon) constructor, so this base class constructor must be called before any reference to self.side
.
Testing the derived classes
To try out the classes defined above, we can create some objects from them. In the main file of the project, we have the code:
from Polygon import * tri = Triangle(5) tri.PrintSides() tri.PrintArea()
The output is Sides = 3 of length 5.0000; Area = 10.8253
. Note that we needed to specify only the side length (5) when creating a Triangle
object; everything else is handled by the constructors.
A slightly more sophisticated example is this:
from Polygon import * from random import * polyClass = [Triangle, Square, Pentagon] polygons = [polyClass[randint(0,2)](random() * 10) for i in range(0, 10)] for poly in polygons: poly.PrintSides() poly.PrintArea()
This little program generates 10 random shapes, each of which has a side length chosen as a random number between 0 and 10. On line 4, we create a list containing the names of the derived classes. On line 5, we use list comprehension to create the list of random polygons. The syntax here begins with polyClass[randint(0,2)]
, which selects at random one of the 3 members of the polyClass
list of derived classes. We then follow it with (random() * 10)
which provides the value of the side length to be passed to the constructor.
Typical output is:
Sides = 4 of length 8.6104; Area = 74.1391 Sides = 4 of length 2.7758; Area = 7.7049 Sides = 4 of length 3.7817; Area = 14.3012 Sides = 3 of length 1.8690; Area = 1.5125 Sides = 4 of length 3.4582; Area = 11.9589 Sides = 3 of length 9.6639; Area = 40.4393 Sides = 4 of length 5.3839; Area = 28.9869 Sides = 5 of length 2.1330; Area = 7.8278 Sides = 5 of length 1.7540; Area = 5.2932 Sides = 3 of length 2.3413; Area = 2.3736
Exercise 2
As part of the design for an adventure game, we’ll look at the code for representing some items carried by the adventurer.
We expect all items to have some properties in common, such as weight and initial location (where they are found in the game). We’ll represent both the weight and location by ints (you can think of the location number as the index of a room or other location in the game). Thus we will need a base class called Item which has data fields for the weight and location of the object.
The game will have a variety of different types of items. We’ll consider two here: Armour and Food. Both of these have weight and location fields, so they can each inherit Item. We’ll add a field called Armour Class (or just ac) to the armour, which is an int specifying how much protection the armour provides. A food item has a Health field, specifying by how much the adventurer’s health is increased (or possibly decreased, if the food is poisoned) when eaten.
To implement encapsulation, the data fields should be private, with appropriate getter and setter methods.
Finally, we want a method in each class that returns a string describing the item. The string should contain the item’s type (class name), and a list of all the data fields and their values for that item. Use method overriding to provide versions of this method in the base and derived classes.
Write some code for these 3 classes, each in its own file. Try to minimize the amount of code duplication by placing code for properties and actions common to all item classes in the base Item class.
To test the code, write a main program that prints a menu of the item types available (we’ll allow only Armour and Food types to be selected, so the base Item class shouldn’t be listed). The user selects an item type and is then prompted to input the data for that item. The user should be allowed to enter as many items as required, and then type ‘quit’ to stop entering items. At this point, a list of all items selected should be printed.
Hint: you may find the built-in function type()
and the built-in data field __name__
useful. Do a bit of online research to learn about them.
The base Item class should look something like this, in Item.py:
class Item: """ base class for items in the game """ def __init__(self): self.__weight = input('Weight: ') self.__location = input('Location: ') def setWeight(self, weight): self.__weight = weight def getWeight(self): return self.__weight def setLocation(self, location): self.__location = location def getLocation(self): return self.__location def PrintProps(self): return type(self).__name__ + '; Weight: ' + self.getWeight() + \ '; Location: ' + self.getLocation()
Since the user is to be asked to input the data as the item is being created, we include input()
calls in the constructor, rather than pass in values as arguments. The base Item class requests the values for weight and location, making them both private.
The setters and getters do nothing special.
The PrintProps()
method on line 19 uses type(self)
to find the class of the object, which could be any of Item or one of the classes derived from it. The __name__
field of this class is just the class’s name as a string.
The Armour class is in Armour.py:
from Item import * class Armour(Item): """an item of armour""" def __init__(self): super().__init__() self.__ac = input('Armour class: ') def getAc(self): return self.__ac def setAc(self, ac): self.__ac = ac def PrintProps(self): props = super().PrintProps() props += '; Armour class: ' + self.getAc() return props
We call the base Item class constructor using super()
on line 6. This asks the user for the weight and location data. Since the armour class is specific to the Armour class, we then ask the user for this on line 7.
The PrintProps()
method on line 15 overrides the version in Item. Its base class version is called first on line 16, following which we add the data from the extra armour class field.
The Food class is in Food.py:
from Item import * class Food(Item): """an item of food""" def __init__(self): super().__init__() self.__health = input('Health: ') def getHealth(self): return self.__health def setHealth(self, health): self.__health = health def PrintProps(self): props = super().PrintProps() props += '; Health: ' + self.getHealth() return props
This class uses the same techniques as Armour.
The main program is:
from Armour import * from Food import * itemTypes = { 1:Armour, 2:Food } itemClasses = [ x.__name__ for x in itemTypes.values() ] items = [] while True: print(f'Available types:') for num in itemTypes.keys(): num = int(num) print(f'{num}. {itemClasses[num - 1]}') choice = input('\nChoose type: ') if choice == 'quit': break choice = int(choice) if choice <= len(itemClasses): items += [ itemTypes[choice]() ] print('\n') carried = [i.PrintProps() for i in items] print('\nCarried:') for c in carried: print(c)
We list the available classes in the dictionary itemTypes
on line 4, with each class indexed by an int as its key. We extract a list of class names in itemClasses
on line 5. Each entry in itemTypes.values()
is a class (not an object from that class), so we can extract its name using __name__
.
On lines 9 to 12, we print out a menu of the available types, and ask the user to choose one. If the choice is valid (checked on line 17) we select the class from the itemTypes
dictionary and call its constructor. The correct constructor is called for each class type, as we’ve set things up in the inheritance hierarchy to work this way.
On line 22, we create a list of the description strings for all the carried items, and then print this out.
A typical session would look like this:
Available types: 1. Armour 2. Food Choose type: 1 Weight: 500 Location: 2 Armour class: 5 Available types: 1. Armour 2. Food Choose type: 2 Weight: 10 Location: 4 Health: 50 Available types: 1. Armour 2. Food Choose type: quit Carried: Armour; Weight: 500; Location: 2; Armour class: 5 Food; Weight: 10; Location: 4; Health: 50