Inheritance 1

Introduction to Software Engineering (CSSE 1001)

Author

Paul Vrbik

Published

March 13, 2025

Introduction

Interface Versus Implementation

(a) Watch Interface
(b) Watch Implementation
Figure 1: The insides and outside of a pocket watch.

Interface

Here the method area is the interface.

rectangle(1.0, 4.0).area()
4.0
circle(1.0).area()  
3.14159

Notice both shapes have the same interface (area) despite requiring different implementations of the area calculation.

How the area was calculated is (mostly) irrelevant at the user-level.

Polymorphism

The word polymorphism means to take on many forms.

In computer science, an interface that works over different underlying data-types is called polymorphic.

The plus operator is polymorphic – it works on numbers, strings, lists, and any other class that has implemented __add__.

2 + 3
5
"Drop" + " Bear"
'Drop Bear'
[1, 2] + [3, 4]
[1, 2, 3, 4]
Point(1,2) + Point(3,4)  # Supposing point was implemented
Point(4, 6)

Exercise 1 What is another example polymorphic function that we have encountered that is not arithmetic?

The length function is polymorphic as well.

len({'a': 1, 'b': 2, 'c': 3})
3
len("Drop Bear")   
9
len([1, 2, 3, 4])
4
len(Vector(3,4))  # Supposing vector was implemented  
5 

Note the “Euclidean distance” between (0, 0) and (3, 4) is 5 – it is the hypotenuse of the 3-4-5 right angle triangle.

Class Polymorphism

Consider a Cat and Dog class that provide the same methods (i.e. have the same interface).

cat = Cat("Maru", 14)
dog = Dog("Bluey", 6)
cat.speak() 
'Meow'
dog.speak()
'Bark'
cat.info()
'Maru the cat is 14 years old'
dog.info()
'Bluey the dog is 6 years old'

Implement the Cat and Dog class so that it provides this interface.

class Cat():
    def __init__(self, name: str, age: int) -> None:
        self._name, self._age = name, age
    
    def speak(self) -> str:
        return "Meow"  # not the same as printing
    
    def info(self) -> str:
        return f"{self._name} the cat is {self._age} years old"
class Dog():
    def __init__(self, name: str, age: int) -> None:
        self._name, self._age = name, age
    
    def speak(self) -> str:
        return "Bark"
    
    def info(self) -> str:
        return f"{self._name} the dog is {self._age} years old"
Note

There are only differences in the Dog and Cat class are what sound is returned by speak and the species of the animal in the info return string.

Inheritance (Is-A)

Notice both of these classes have very similar implementations of their interfaces. It would be best to reuse the code by having generic methods instead.

For instance, Cat and Dog have identical __init__ methods and therefore it is overkill to copy-paste this for them and every other animal we intend to instantiate.

Objects are already grouped by their classes. For instance, we could instantiate many Dog objects from the class Dog.

In maths say: \[ {\rm Bluey} \in Dog. \] In CS we say: \[ {\rm Bluey} \text{ is a } Dog \] We can also group classes themselves into super-classes \[ {\rm Dog} \text{ is a } Animal \] \[ {\rm Cat} \text{ is a } Animal \] and have Animal define the generic interface that Dog and Cat share.

Hidden class attributes

For the following object.

bluey = Dog("Bluey", 6.0)
type(bluey)
__main__.Dog

It is possible to extract the class name.

type(bluey).__name__
'Dog'
type(bluey).__name__.lower()  
'dog'

This line composition (i.e. applying several methods to the same instance) is an example of the clarity OOP affords us. We perform a sequence of explicitly defined transformations on an object inline.

class Animal():
    def __init__(self, name: str, age: int) -> None:
        self._name, self._age = name, age
        return
    
    def info(self) -> str:
        animal_type = type(self).__name__.lower()
        return f"{self._name} the {animal_type} is {self._age} years old"

The Animal class is a normal object and behaves as such:

bluey = Animal("Bluey", 6)
maru = Animal("Maru", 14)
bluey.info()
'Bluey the animal is 6 years old'
maru.info() 
'Maru the animal is 14 years old'
class Cat(Animal):  # Cat inherits from Animal
    def speak(self) -> str:
        return "Meow"

class Dog(Animal):  # Dog inherits from Animal
    def speak(self) -> str:
        return "Bark"

The Dog object will work as if the inherit code (highlighted) is present.

class Dog(Animal):  # Dog inherits from Animal
    
    def __init__(self, name, age) -> None:
        self._name, self._age = name, age
    
    def info(self) -> str:
        animal_type = type(self).__name__.lower()
        return f"{self._name} the {animal_type} is {self._age} years old" 
    
    def speak(self) -> str:
        return "Bark"
maru = Cat("Maru", 16)
bluey = Dog("Bluey", 6)
maru.speak()
'Meow'
bluey.speak()
'Bark'
maru.info()
'Maru the cat is 16 years old'
bluey.info()
'Bluey the dog is 6 years old'

Abstract Classes

But notice both Cat and Dog require Speak, albeit a custom one. We can express this in Animal by implementing a Speak method that throws the NotImplementedError.

class Animal():
   def speak(self) -> str:
        raise NotImplementedError

This abstract class is essentially a blueprint for what subclasses of Animal must implement in order to share the common Animal-interface.

What does a turtle sound like!?

class Turtle(Animal):
     pass
yertle = Turtle("Yertle", 101)
yertle.info()
'Yertle the turtle is 101 years old'
yertle.speak()  
Error:  NotImplementedError

What does the fox say?

class Fox(Animal):
    def speak(self) -> str:
        return "Ring-ding-ding-ding-dingeringeding!"
kyuubi = Fox("Kyuubi", 1432)
kyuubi.info()  
'Kyuubi the fox is 1432 years old'
kyuubi.speak()  
'Ring-ding-ding-ding-dingeringeding!'

Overwriting Methods

We are able to overwrite methods by declaring them (as we normally would).

class Fox(Animal):
    def speak(self) -> str:
        return "Ring-ding-ding-ding-dingeringeding!"
    
    def info(self) -> str:
        return "Foxes are better than cats and dogs."
    
fox.info() 
'Foxes are better than cats and dogs'

Extending Methods

We are also able to extend methods.

class Fox(Animal):
    def __init__(self, name: str, age: int, nationality: str) -> None:
        super().__init__(name, age)  # call init from the super-class
        self._nationality = nationality # extra attribute

kyuubi = Fox("Kyuubi", 1432, "Japanese")
kyuubi._name 
'Kyuubi'
kyuubi._age 
1432
kyuubi._nationality 
'Japanese'

UML Diagrams

A unified modelling language diagram is the standard way of illustrating inheritance relationships among classes.

For instance, for our Animal class we have…

Figure 2: The uml diagram for our animal example.

Method Resolution Order

Consider the following code snippet (from the course notes).

class A(object):  # 'object' is the universal class
    def __init__(self, x):
        self.x = x
    def f(self):
        return self.x
    def g(self):
        return 2 * self.x
    def fg(self):
        return self.f() - self.g()
a = A(3)
a.x
3
a.f()
3
a.g()
6
a.fg()
-3
class B(A):
    def __init__(self, x):  # inherit from A
        self.x = x
    
    def f(self):  # inherit from A
        return self.x
    
    def g(self):  # overwrite
        return self.x ** 2
    
    def fg(self):  # inherit from A
        return self.f() - self.g()
class B(A):  # inherit from A
    def g(self):  # overwrite
        return self.x ** 2
b = B(7)
b.x
7
b.f()
7
b.g()
49
b.fg()
-42
class C(B):  
    def __init__(self, x, y):  # extend from B from A
        super().__init__(x)  # Super is B
        self.y = y
    
    def fg(self):  # extend from B from A
        return super().fg() * self.y
class C(B):  
    def __init__(self, x, y):  # extend from B from A
        super().__init__(x)
        self.y = y
    
    def f(self):  # inherit from B from A
        return self.x
    
    def g(self):  # inherit from B
        return self.x ** 2
    
    def fg(self):  # extend from B from A
        return super().fg() * self.y
c = C(3,5)
c.x
3
c.y
5
c.f()
3
c.g()
9
c.fg()
-30
class D(A):  # inherit from A
    def f(self):  # overwrite
        return -2 * self.g()
class D(A):  # inherit from A
    def __init__(self, x):  # inherit from A
        self.x = x
    
    def f(self):  # overwrite
        return -2 * self.g()
    
    def g(self):  # inherit from A
        return 2 * self.x
    
    def fg(self):  # inherit from A
        return self.f() - self.g()
d = D(3)
d.x    
3
d.f()    
-12
d.g()    
6
d.fg()    
-18
Figure 3: The UML diagram for the A, B, C, D class example.

Practice Assignment UML Class Diagram

Dotted arrows indicate has-a relationships and solid ones denote is-a relationships.

Exercises

Exercise 2 Implement the following classes that inherit from Shape.

class Shape():
    pass

class Rectangle(Shape):
    pass

class Square(Shape):
    pass

Which provides the following interface.

r = Rectangle(10, 5)
s = Square(10)
r < s
False

Summary

Classes are further categorized into super-classes which provide abstract-methods. This abstract methods serve as blueprints or outright implementations for the interfaces that all sub-classes must provide.