18. Inheritance#

Inheritance is a fundamental concept in object-oriented programming (OOP). Inheritance allows a class to inherit attributes and methods from another class, promoting code reuse and a hierarchical class structure. Inheritance enables the creation of a new class that is a modified version of an existing class. This new class, called the subclass, inherits attributes and methods from an existing class, called the superclass. The subclass can also add new attributes and methods or override existing ones. Consider a simple hierarchy where Employee is a specialized version of Person.


class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def greeting(self):
        print(f"Hello, my name is {self.first_name} {self.last_name}.")

class Worker(Person):
    def __init__(self, first_name, last_name, age, worker_id):
        super().__init__(first_name, last_name)  # Call the constructor of the superclass
        self.age = age
        self.worker_id = worker_id

    def greeting(self):
        print(f"Hello, my name is {self.first_name} {self.last_name}, my id is {self.worker_id}.")

In this example:

  • Person is the superclass with basic attributes and methods.

  • Worker is the subclass that extends Person by adding new attributes (age, worker_id) and overriding the greeting method to include additional information.

  • super is used to call the __init__ method of the superclass Person to initialize inherited attributes.

  • The greeting method is overridden in Worker to provide a more specific implementation.

In Python, every class implicitly inherits from the base object class, even if it’s not explicitly mentioned.

  • Superclass (Parent Class): The class being inherited from. In our example, Person is the superclass of Worker.

  • Subclass (Child Class): The class that inherits from another class. In our example, Worker is the subclass of Person.

18.1. A more complex example#

Let’s explore a more complex example involving polygons. First, we define a Point class to represent a point in a 2D space with methods to compute the Euclidean distance between two points.

import math

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def distance(self, other):
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)

Next, we’ll define a Polygon class, which will serve as a general framework for all polygon types. The Polygon class will contain a list of Point objects representing its vertices.

class Polygon:
    def __init__(self, points):
        self.points = points

    def number_of_sides(self):
        return len(self.points)
    
    def perimeter(self):
        perim = 0
        for i in range(len(self.points)):
            perim += self.points[i].distance(self.points[(i + 1) % len(self.points)])
        return perim

We can now create specific types of polygons, such as Triangle and Square, which inherit from the Polygon class. Each subclass will override or extend methods from Polygon to provide specific functionality, such as calculating the area.

class Triangle(Polygon):
    def __init__(self, a, b, c):
        super.__init__([a, b, c])

    def area(self):
        p1, p2, p3 = self.points
        a = p1.distance(p2)
        b = p2.distance(p3)
        c = p3.distance(p1)
        s = (a + b + c) / 2
        return math.sqrt(s * (s - a) * (s - b) * (s - c))

class Square(Polygon):
    def __init__(self, a, b, c, d):
        super.__init__([a, b, c, d])

    def area(self):
        side = self.points[0].distance(self.points[1])
        return side ** 2

Here, both Triangle and Square inherit from Polygon and leverage its perimeter method, while providing their own implementation for the area method.

18.2. Abstract method#

The challenge with our current setup is that the Polygon class does not have a generic way to compute the area. Thus, if we introduce a new class pentagon, even if it inherit from Polygon, it may not have an area method. To ensure that every specific Polygon subclass implements the area method, we have to introduce area in Polygon. Here, is a first proposition to do it.

class Polygon:
    def __init__(self, points):
        self.points = points

    def number_of_sides(self):
        return len(self.points)
    
    def perimeter(self):
        perim = 0
        for i in range(len(self.points)):
            perim += self.points[i].distance(self.points[(i + 1) % len(self.points)])
        return perim

    def area(self):
        raise NotImplementedError

Now, we can override this method for triangle and square.

class Triangle(Polygon):
    def __init__(self, a, b, c):
        super().__init__([a, b, c])

    def area(self):
        p1, p2, p3 = self.points
        a = p1.distance(p2)
        b = p2.distance(p3)
        c = p3.distance(p1)
        s = (a + b + c) / 2
        return math.sqrt(s * (s - a) * (s - b) * (s - c))

class Square(Polygon):
    def __init__(self, a, b, c, d):
        super().__init__([a, b, c, d])

    def area(self):
        side = self.points[0].distance(self.points[1])
        return side ** 2

The problem with this appproach is that the area method raise an NotImplementedError when called on a Polygon. Thus it should be forbiden to create a polygon, in order to prevent this error.

h = Polygon([Point(1, 0), Point(0, 1), Point(1, 2), Point(3, 1), Point(2, 0)])
h.area()

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[25], line 2
      1 h = Polygon([Point(1, 0), Point(0, 1), Point(1, 2), Point(3, 1), Point(2, 0)])
----> 2 h.area()

Cell In[15], line 15, in Polygon.area(self)
     14 def area(self):
---> 15     raise NotImplementedError

NotImplementedError: 

We can deal with it with a try except block but it is not recommanded. The best way is to make the class Polygon asbtract. This means that some methods are not implemented and that you can not create object of that type. You can create object of a sub class but not that specific class. To implement an abstract class we need the module abc.

from abc import ABC, abstractmethod

class MyABC(ABC):
    pass

Then you can add an abstract method by using a decorator. The decorator is the line beginning with @. We will not discuss it in this course, we use it as an idiomatic.

    @abstractmethod
    def method(self):
        pass

For our Polygon class we have now:

from abc import ABC, abstractmethod

class Polygon(ABC):
    def __init__(self, points):
        self.points = points

    def number_of_sides(self):
        return len(self.points)
    
    def perimeter(self):
        perim = 0
        for i in range(len(self.points)):
            perim += self.points[i].distance(self.points[(i + 1) % len(self.points)])
        return perim

    @abstractmethod
    def area(self):
        pass

By inheriting from ABC (Abstract Base Class), and marking area as an abstract method using the @abstractmethod decorator, we ensure that the Polygon class cannot be instantiated directly. Only its subclasses, which provide a concrete implementation of area, can be instantiated.

>>> t = Triangle(Point(0, 0), Point(1, 1), Point(2, 0))
>>> s = Square(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0))
>>> t.area()
0.9999999999999996
>>> s.area()
1
>>> h = Polygon([Point(1, 0), Point(0, 1), Point(1, 2), Point(3, 1), Point(2, 0)])
>>> h.area()

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 1
----> 1 h = Polygon([Point(1, 0), Point(0, 1), Point(1, 2), Point(3, 1), Point(2, 0)])
      2 h.area()

TypeError: Can't instantiate abstract class Polygon with abstract method area

18.3. Arguments for Using Inheritance in Python#

Polymorphism: Inheritance enables polymorphism, which allows objects of different classes to be treated as objects of a common superclass. This is particularly useful for writing flexible and generic code, such as implementing functions or methods that can operate on objects of various derived classes.

Abstraction: Inheritance allows for the abstraction of general concepts into a base class. Derived classes can then focus on more specific details, simplifying the development process by separating the high-level design from the low-level implementation details.

DRY Principle: Inheritance supports the “Don’t Repeat Yourself” (DRY) principle by allowing shared code to be defined in one place. This not only reduces errors but also simplifies the process of updating and refactoring the code. By centralizing common functionality in a base class, inheritance can make maintaining the code easier. Changes to the common functionality need to be made only in one place, the base class, and they automatically propagate to all derived classes.

Organized Code Structure: Inheritance helps in organizing code by creating a natural hierarchy of classes. This makes the codebase more structured and easier to navigate, especially in large projects where classes can be logically grouped into a parent-child relationship. In team environments, inheritance can improve collaboration by making it clear how different parts of the system are related. Team members can work on different derived classes knowing they share a common base, which facilitates understanding and integration of the code.

Extensibility: It allows for easy extension of existing code. New features can be added to a derived class without modifying the existing code in the base class, following the Open/Closed Principle (OCP) which states that software entities should be open for extension but closed for modification.