Inheritance

A final topic for us to discuss in this introduction to object oriented programming is the concept of inheritance. Working with inheritance provides powerful abstractions and elegant code re-use - it permits a class to inherit and build off of the attributes of another class.

Let’s immediately consider an example of inheritance in action. Let’s revisit the Rectangle class that we wrote in the introduction to this module.

class Rectangle:
    """ A class of Python object that describes the properties of a rectangle"""
    def __init__(self, width, height, center=(0, 0)):
        self.width = width
        self.height = height
        self.center = center

    def __repr__(self):
        return "Rectangle(width={w}, height={h}, center={c})".format(h=self.height,
                                                                     w=self.width,
                                                                     c=self.center)

    def compute_area(self):
        return self.width * self.height

Now suppose that we also want to write a Square class, such that only a single side length need be specified to determine its size. Recognize that a square is a special type of rectangle - one whose width and height are equal. In light of this, we ought to leverage the code that we already wrote for Rectangle. We can do this by defining a Square class that is a subclass of Rectangle. This means that Square will inherit all of the attributes of Rectangle, including its methods. Let’s proceed with writing this subclass:

# Creating Square, a subclass of Rectangle
class Square(Rectangle):
    def __init__(self, side, center=(0, 0)):
        # equivalent to `Rectangle.__init__(self, side, side, center)`
        super().__init__(side, side, center)

Specifying class Square(Rectangle) signals that Square is a subclass of Rectangle and thus it will have inherited the attributes of Rectangle. Next, see that we overwrote the __init__ method that Square inherited; instead of accepting a height and a width, Square should by specified by a single side length. Within this new __init__ method, we pass in that single side length as both the width and height to Rectangle.__init__. super always refers to the “super class” or “parent class” of a given class, thus super is Rectangle here.

Having defined our subclass, we can leverage the other methods of Rectangle as-is. Let’s see Square in action:

# create a square of side-length 2
>>> my_square = Square(2)

# using the inherited `compute_area` method
>>> my_square.compute_area()
4

# a square is a rectangle with equal height/width
>>> my_square
Rectangle(width=2, height=2, center=(0.0, 0.0))

>>> my_square.width == my_square.height
True

The built-in issubclass function allows us to verify the relationship between Square and Rectangle.

# `Square` and `Rectangle` are distinct classes
>>> Square is not Rectangle
True

# `Square` is a subclass of `Rectangle`
>>> issubclass(Square, Rectangle)
True

# `my_square is an both an instance of `Square` and `Rectangle`
>>> isinstance(my_square, Square)
True

>>> isinstance(my_square, Rectangle)
True

Summary of Inheritance

In general, if you have a class A, then you can define a subclass of A via:

class A:
    attr = 0

    def method(self):
        return 0

# `B` is a subclass of `A`
class B(A):
    # inherits `attr` and `method`
    b_attr = -2  # class attribute distinct to `B`

    def method(self):
        # overwrites inherited `method`
        return -1

B will have inherited all of the attributes and methods of A. Defining attributes and methods within the definition of B will overwrite those that already exist in A. B is also free to have its own distinct attributes and methods be defined, irrespective of A.

>>> issubclass(B, A)
True

>>> A.attr
0

>>> A().method()
0

>>> B.attr
0

>>> B().method()
-1

>>> B.b_attr
-2

We have only scratched the surface of the topic of class inheritance. That being said, this section does convey the essential functionality and utility of class inheritance.