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 extendsPerson
by adding new attributes (age
,worker_id
) and overriding thegreeting
method to include additional information.super
is used to call the__init__
method of the superclassPerson
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 ofWorker
.Subclass (Child Class): The class that inherits from another class. In our example,
Worker
is the subclass ofPerson
.
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.