Inheritance#

This page disscusses various features of the python related to inheritance.

You can find helpful details in the Classes python tutorial.

Linearization#

Sometimes in the python literature the term “linearization”. Linearization is an ordered list of classes where each class searches for attributes and takes an attribute from the first class in which it appears.

Python classes keep their linearization in the __mro__ attribute.


The following cell shows the output of the __mro__ attribute for the int type.

int.__mro__
(int, object)

Or more complex linearization for the sklearn.linear_model.LinearRegresssion.

from sklearn.linear_model import LinearRegression
LinearRegression.__mro__
(sklearn.linear_model._base.LinearRegression,
 sklearn.base.MultiOutputMixin,
 sklearn.base.RegressorMixin,
 sklearn.linear_model._base.LinearModel,
 sklearn.base.BaseEstimator,
 sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin,
 sklearn.utils._metadata_requests._MetadataRequester,
 object)

Multiple inheritance#

In python you are able to inherit your class from multiple classes. But with this feature come some complexity - it’s hard to define the order of searching for attributes in the ancestors.

There is a detailed explanation of how it works in modern python, as well as a justification for why it is done this way on this page Method Resolution Order.

It uses “C3” method resolution order, which can result in non-trivial resolution order. It clearly described in corresponding section of documentation.


The following cell implements example that will help crusial feature of the C3 linearalization method.

O = object
class E(O): pass
class D(O):
    def method(self): print("Methon of D") 
class C(E):
    def method(self): print("Method of C")
class B(D): pass

Here is the corresponding schema.

                              ---
    Level 3                  | O |
                              --- 
                            /     \
                           /       \
                          ---     ---
    Level 2              | D |   | E |
                          ---     ---
                           |       |
                           |       |
                          ---     ---
    Level 1              | B |   | C |
                          ---     ---
                           \       /
                            \     /
                              ---
    Level 0                  | A |
                              ---

The following cell inherits from the classes B and C class A. It seems that A have to load method from C (as it closest) definition of method.

class A(B, C): pass
a = A()
a.method()
Methon of D

But in fact, it uses the implementation that comes from D. This happens according to the C3 linearization rule. Specifically, in this case, D does not appear in the ancestors of C, so for A, it is the unique ancestor of B that has higher priority (since it is declared first in the definition of A).

But if C somehow inherits from D, A will use method from C. The following cell changes inheritance hierarchy accordingly.

class C(E, D):
    def method(self):
        print("Method of C")

class A(B, C): pass
a = A()
a.method()
Method of C

A takes method from C. It’s convenient to think of it as “C redefined method from D”, so now it uses method from C.

Child defining logic#

You can define some custom logic for the creating for the child classes by using the __init_subclass__ method.

Check official description of the method.


The following cell creates a Parent class that makes all it’s child classes to print themselves just the moment they are defined.

class Parent:
    def __init_subclass__(cls):
        super().__init_subclass__()
        print("My type is:", cls)

The following cell shows the process of inheriting the Parent class.

class Child(Parent): pass
My type is: <class '__main__.Child'>

As a result, it prints the object that holds the Child class.

Required attributes#

A common pattern for using these features is to define attributes that need be implemented in the child classes.


The following cell implements in the __init_subclass__ code that checks if the foo attribute is defined in the child classes and throws NotImplementedError if it is not.

class Parent:
    def __init_subclass__(cls):
        if not hasattr(cls, 'foo'):
            raise NotImplementedError("foo is not implemented")

The following cell shows how the creation of the Fail(Parent) class that doesn’t define the foo attribute fails.

try: 
    class Fail(Parent): pass
except NotImplementedError as e:
    print(e)
foo is not implemented

The next code shows the creation of the child that implements the foo attribute - all goes well.

class Ok(Parent):
    foo = 42