Python super() and Multiple Inheritance
About super()
super()
super(type, object_or_type=None)
A parent class is a class that has other classes derived from it. Sibling classes are classes that share a common parent.
# Child1 and Child2 are siblings clas Parent: pass class Child1(Parent): pass class Child2(Parent): pass
In multiple inheritance, parent classes can also have a common parent class (the diamond problem).
# Parent1 and Parent2 are siblings class A: pass clas Parent1(A): pass clas Parent2(A): pass class Child(Parent1, Parent2): pass
Hierarchy of Related Classes With super()
The following example illustrates the use of super()
in a class hierarchy.
All classes have a superclass. All subclasses of class A
have common interface: "self.attr" and "self.method". Derived classes can specialize the interface, providing a specific implementation where necessary.
super()
is useful for accessing inherited methods that have been overridden in a class.
In this case, we call parent.__init__
in each child class and parent.method
in class D
.
from abc import ABCMeta, abstractmethod class A(metaclass=ABCMeta): def __init__(self, attr): print("A.__init__") self.attr = attr @abstractmethod def method(self): pass class B(A): def __init__(self, attr): print("B.__init__") # super().__init__(attr) -> A.__init__(self, attr) super().__init__(attr) def method(self): print("B method") class C(A): def __init__(self, attr): print("C.__init__") # super().__init__(attr) -> A.__init__(self, attr) super().__init__(attr) def method(self): print("C method") class D(B): def __init__(self, attr): print("D.__init__") # super().__init__(attr) -> B.__init__(self, attr) super().__init__(attr) def method(self): # super().method() -> super(D, self).method() print("D method") super().method()
Creating instances.
b = B(1) # B.__init__ # A.__init__ b.method() # B method c = C(2) # C.__init__ # A.__init__ c.method() # C method d = D(3) # D.__init__ # B.__init__ # A.__init__ d.method() # D method # B method
A derived class inherits all the methods, properties, and data attributes of the base class.
print(dir(d)) # [..., 'attr', 'method'] print(isinstance(d, A)) # True print(isinstance(d, B)) # True # type's method resolution order print(D.mro()) # [<class '__main__.D'>, <class '__main__.B'>, # <class '__main__.A'>, <class 'object'>]
Class D
is also a subclass of A
, but not directly.
print(D.__bases__) # (<class '__main__.B'>,) # list of immediate subclasses print(A.__subclasses__()) # [<class '__main__.B'>, <class '__main__.C'>] print(B.__subclasses__()) # [<class '__main__.D'>] print(issubclass(D, A)) # True print(issubclass(D, B)) # True
super() and Multiple Inheritance
Chain of Class.__init__
Calls
Python supports multiple inheritance. Multiple inheritance means that a class directly derives from multiple superclasses at the same time. These superclasses may not be related to each other.
In the following example, class B
derives from two classes at the same time, and we want to use super()
. This code works, but we can notice that A2.__init__
is not called. This can be a problem if we need to pass some argument to A1.__init__
and A2.__init__
or if we rely on some code being executed in A2.__init__
.
class A1: def __init__(self): print("A1.__init__") def method(self): print(self) class A2: def __init__(self): print("A2.__init__") def other_method(self): print(self) class B(A1, A2): def __init__(self): super().__init__() b = B() # A1.__init__ print(dir(b)) # [..., 'method', 'other_method'] b.method() # <__main__.B object at 0x00000213C0056E50> b.other_method() # <__main__.B object at 0x00000213C0056E50>
Here we encounter the first constraint: all classes in the hierarchy must use super()
. To comply with this, we can add super().__init__()
to classes A1
and A2
. All Python3 classes implicitly have an object
as their parent class, so we can call super().__init__()
for A1
and A2
.
class A1: def __init__(self): print("A1.__init__") super().__init__() def method(self): print(self) class A2: def __init__(self): print("A2.__init__") super().__init__() def other_method(self): print(self) class B(A1, A2): def __init__(self): super().__init__() b = B() # A1.__init__ # A2.__init__
But if we want to use some class from PSL or class from an external library in multiple inheritance, we cannot guarantee that all classes use super
.
Passing Different Arguments
Another limitation arises if we need to pass some arguments to the __init__
methods. Each superclass may require a different number of values of attributes.
We can't just pass these values to super().__init__(<attrs list>)
. We will get a TypeError
due to a mismatch of arguments with the __init__
parameters in superclasses.
class A1: def __init__(self, attr1): print("A1.__init__") super().__init__() self.attr1 = attr1 def method(self): print(self) class A2: def __init__(self, attr2, attr3): print("A2.__init__") super().__init__() self.attr2 = attr2 self.attr3 = attr3 def other_method(self): print(self) class B(A1, A2): def __init__(self, attr1, attr2, attr3): super().__init__() # TypeError: A1.__init__() missing 1 required positional argument: 'attr1' # super().__init__(attr1) # TypeError: A2.__init__() missing 2 required positional arguments: 'attr2' and 'attr3' # super().__init__(attr1, attr2, attr3) # TypeError: A1.__init__() takes 2 positional arguments but 4 were given b = B("attr1","attr2","attr3")
Solutions
Passing arguments to __init__
: *args, **kwargs
To pass arguments through the __init__
call chain, we can use *args
and **kwargs
in class A1
. Since there are no other classes after A2
, we can leave it as is.
This option is suitable when you use your custom classes.
class A1: def __init__(self, attr1, *args, **kwargs): print("A1.__init__") super().__init__(*args, **kwargs) self.attr1 = attr1 def method(self): print(self) class A2: def __init__(self, attr2, attr3): print("A2.__init__") super().__init__() self.attr2 = attr2 self.attr3 = attr3 def other_method(self): print(self) class B(A1, A2): def __init__(self, attr1, attr2, attr3): super().__init__(attr1, attr2, attr3) b = B("attr1","attr2","attr3") # A1.__init__ # A2.__init__ print(b.attr1, b.attr2, b.attr3) # attr1 attr2 attr3
Calling __init__
of Each Superclass Separately
This option is suitable for both cases: when you use your custom classes, or when you use some external classes.
import tkinter as tk class A: def __init__(self, attr): print("A.__init__") self.attr = attr def method(self): print(self) class B(tk.Tk, A): def __init__(self, attr): tk.Tk.__init__(self) A.__init__(self, attr) b = B("attr") # A.__init__ print(b.attr) # attr print(b.master) # None
Using Mixin Classes
Consider using mixins in multiple inheritance. A mixin is a class that contains only methods that provide additional functionality to other classes. Its methods are intended to be included or mixed with a child class.
A mixin does not define the structure and behavior of derived classes. It doesn't have any specific data attributes, so we don't need to call it's __init__
. For simplicity, a mixin may not be derived from other classes.
Mixins are often used when you want to add the same features to many classes. These features are already implemented in the mixin.
class A: def __init__(self, attr): print("A.__init__") super().__init__() self.attr = attr def method(self): print(self) class SomeMixin: # class attributes or properties may exist attr = "attr" @property def some_property(self): print(self.__class__.__name__) def other_method(self): print(self) class B(SomeMixin, A): def __init__(self, attr): super().__init__(attr) b = B("attr") # A.__init__ b.method() # <__main__.B object at 0x00000213C0056E50> b.other_method() # <__main__.B object at 0x00000213C0056E50> b.some_property # B
References: