Single inheritance and the super() function: the basics

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 answer

An 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.

See answer

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

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.