---
jupytext:
  formats: content_md///md:myst,content///ipynb
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.13.8
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

(python:inheritance)=
# Inheritance

Inheritance is an important principle in object-oriented programming. Although we may reach our aims without using inheritance in our own code, it's important to know the concept and corresponding syntax constructs to understand other people's code. We'll meet inheritance related code in
* the documentation of modules and packages,
* when customizing and extending library code.

Related exercises: [](exercises:python:oop).

## Principles of Object-Oriented Programming

Up to now we only considered three of four fundamental OOP principles:
* *encapsulation*: code is structured into small units (write functions for different tasks and group them together with relevant data into objects)
* *abstraction*:
  * similar tasks are handled in one and the same way (instead of several unrelated objects we define a class and instantiate several objects sharing the same interface)
  * implementation details are hidden behind interfaces (a class defines an interface by providing methods and (non-private) member variables; concrete implementation of methods and usage of member variables are not of importance and invisible from outside)
* *polymorphism* (functions and method accept different sets and types of arguments, that is, interfaces are flexible; something a Python programmer does not care about because it's a very native Python feature, in contrast to C/C++, for instance)
  
The missing principle is
* *inheritance* (create new classes by extending existing classes)

## Idea and Syntax

Inheritance is a technique to create new classes by extending and/or modifying existing ones. A new class may have a *base class*. The new class inherits all methods and member variables from its base class and is allowed to replace some of the methods and to add new ones. Syntax:
```python
class NewClass(BaseClass):
    
    def additional_method(self, args):
        # do something
    
    def replacement_for_base_class_method(self, args):
        # do something
```
The only difference compared to usual class definitions is in the first line, where a base class can be specified. Defining methods works as before. If the method name does not exist in the base class, then a new method is created. If it already exists in the base class, the new one is used instead of the base class' method. In addition to explicitly defined methods, the new class inherits all methods from the base class.

Inheritance saves time for implementation and leads to a well structured class hierachy. Object-oriented programming is not solely about defining classes (encapsulation and abstraction), but also about defining meaningful relations between classes, thus, to some extent mapping real world to source code.

## Example

Real-life examples of inheritance often are quite involved. For illustration we use a pathological example resampling relations between geometric objects.

Imagine a vector drawing program. Each geometric object shall be represented as object of a corresponding class. Say quadrangles are objects of type `Quad`, paraxial rectangles are objects of type `ParRect` and so on. Let's start with class `Point`:

```{code-cell} ipython3
class Point:
    ''' represent a geometric point in two dimensions '''
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f'({self.x}, {self.y})'
```

Now we define `Quad`:

```{code-cell} ipython3
class Quad:
    ''' represent a quadrangle '''
    
    def __init__(self, a, b, c, d):
        ''' make a quad from 4 Point objects '''
        self._a = a
        self._b = b
        self._c = c
        self._d = d
    
    def get_points(self):
        return (self._a, self._b, self._c, self._d)
    
    def __str__(self):
        return f'quadrangle with points' \
               f'({self._a.x}, {self._a.y}), ({self._b.x}, {self._b.x}), ' \
               f'({self._c.x}, {self._c.x}), ({self._d.x}, {self._d.x})'
```

The member variables `_a`, `_b`, `_c`, `_d` are hidden since we consider them implementation details. If the user wants access to the four points making the quadrangle, `get_points` should be called. This way we are free to store the quadrangle in a different format if it seems resonable in future when extending class' functionality. This is a design decision and is in no way related to inheritance.

Here comes `ParRect`: Note that a paraxial rectangle is defined by two Points.

```{code-cell} ipython3
class ParRect(Quad):
    
    def __init__(self, a, c):
        ''' make a paraxial rect from two points '''
        b = Point(c.x, a.y)
        d = Point(a.x, c.y)
        super().__init__(a, b, c, d)
        
    def __str__(self):
        return f'paraxial rect with points ({self._a.x}, {self._a.y}), ({self._c.x}, {self._c.x})'
    
    def area(self):
        ''' return the rect's area '''
        return abs(self._b.x - self._a.x) * abs(self._d.y - self._a.y)
```

The `ParRect` class inherits everything from `Quad`. It has a new constructor with fewer arguments than in `Quad`, but calls the constructor of `Quad`.

```{important}
The built-in function `super` in principle returns `self` (that is, the current object), but redirects method calls to the base class.
```

We reimplement `__str__` and add the new method `area`. Note that `ParRect` objects have member variables `_a`, `_b`, `_c`, `_d` since those are created by the `Quad` constructor we call in the `ParRect` constructor. Also the `get_points` method is a  member of `ParRect` since it gets inherited from `Quad`.

```{code-cell} ipython3
parrect = ParRect(Point(0, 0), Point(2, 1))

print(parrect)

print('area: {}'.format(parrect.area()))

a, b, c, d = parrect.get_points()
print('all points:', a, b, c, d)
```

## Type Checking

Note that `isinstance` also returns `True` if we check against a base class of an object's class. In other words, each object is an instance of its class and of all base classes.

```{code-cell} ipython3
print(isinstance(parrect, ParRect))
print(isinstance(parrect, Quad))
```

In contrast, type checking with `type` returns `False` if checked against the base class:

```{code-cell} ipython3
print(type(parrect) == ParRect)
print(type(parrect) == Quad)
```

## Every Class is a Subclass of `object`

In Python there is a built-in class `object` and every newly created class automatically becomes a subclass of `object`. The line
```python
class my_new_class:
```
is equivalent to
```python
class my_new_class(object):
```
and also to
```python
class my_new_class():
```
by the way.

To see this in code we might use the built-in function `issubclass`. This function returns `True` if the first argument is a subclass of the second.

```{code-cell} ipython3
class MyClass:
    
    def __init__(self):
        print('Here is __init__()!')

print(issubclass(MyClass, object))
```

Alternatively, we may have a look at the `__base__` member variable, which stores the base class:

```{code-cell} ipython3
print(MyClass.__base__)
```

Objects of type `object` do not have real functionality. The `object` class provides some auxiliary stuff used by the Python interpreter for managing classes and objects.

```{code-cell} ipython3
obj = object()
dir(obj)
```

## Virtual Methods

Python does not allow for directly implementing so called *virtual methods*. A virtual method is a method in a base class which has to be (re-)implemented by each subclass. The typical situation is as follows: The base class implements some functionality, which for some reason has to call a method of a subclass. How to guarantee that the subclass provides the required method?

In Python a virtual method is a usual method which raises a `NotImplementedError`, a special exception type like `ZeroDivisionError` and so on. If everything is correct, this never happens, because the subclass overrides the base class' method. But if the creator of the subclass forgets to implement the method required by the base class, an error message will be shown.

## Multiple Inheritance

A class may have several base classes. Just provide a tuple of base classes in the class definition:
```python
class my_class(base1, base2, base3):
```
The new class inherits everything from all its base classes.

If two base classes provide methods with identical names, the Python interpreter has to decide which one to use for the new class. There is a well-defined algorithm for this decision. If you need this knowledge someday, watch out for *method resolution order (MRO)*.

(python:inheritance:exceptions)=
## Exceptions Inherit from `Exception`

Up to now we used built-in exceptions only, like `ZeroDivisionError`. But now we have gathered enough knowledge to define new exceptions. Exeptions are classes as we noted before. Each exception is a direct or indirect subclass of `BaseException`. Almost all exceptions also are a subclass of `Exception`, which itself is a direct subclass of `BaseException`. See [Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) for exceptions' genealogy.

If we want to introduce a new exception, we have to create a new subclass of `Exception`.

```{code-cell} ipython3
class SomeError(Exception):
    
    def __init__(self, message):
        self.message = message
    
def my_function():
    print('I do something...')
    raise SomeError('Meaty error message!!!')
    
print('Entering my_function...')
try:
    my_function()
except SomeError as error:
    print('Exception SomeError: {}'.format(error.message))
```

At first we define a new exception class `SomeError`. The constructor takes an error message and stores it in the member variable `message`.
The function `my_function` raises `SomeError`.
The main program catches this exception and prints the error message. The `as` keyword provides access to a concrete `SomeError` object containing the error message.

Note that `except SomeBaseClass` also catches all subclasses of `SomeBaseClass`. If we want to handle a subclass exception separately we have to place its `except` line above the base class's `except` line.
Contrary, a subclass `except` never handles a base class exception.
