Множественное наследование, функция super

Продолжаем тему наследования классов в Python и поговорим о механизме множественного наследования.

Множественное наследование – это возможность построения дочернего класса на основе нескольких базовых.

Вот хорошая иллюстрация этого принципа: скрещивая лошадь с ослом, получаем мула.

В качестве примера множественного наследования построим класс графического примитива Line на основе двух классов: Styles и Pos. Если мы все запишем вот так:

class Point:
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y
 
    def __str__(self):
        return f"({self.__x}, {self.__y})"
 
class Styles:
    def __init__(self, color = "red", width = 1):
        print("Конструктор Styles")
        self._color = color
        self._width = width
 
class Pos:
    def __init__(self, sp:Point, ep:Point):
        print("Конструктор Pos")
        self._sp = sp
        self._ep = ep
 
class Line(Pos, Styles):
    def draw(self):
        print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")
 
l = Line( Point(10, 10), Point(100, 100), "green", 5 )

То при запуске программы увидим ошибку, т.к. первый вызываемый конструктор класса Pos принимает всего два аргумента, а мы передаем четыре. Давайте поправим этот момент и запишем его в таком виде:

class Pos:
    def __init__(self, sp:Point, ep:Point, *args):
        print("Конструктор Pos")
        self._sp = sp
        self._ep = ep
        Styles.__init__(self, *args)

Мы здесь добавили остаточные аргументы *args и далее, вызвали конструктор второго базового класса Styles из класса Pos с этими остаточными аргументами. В результате у нас получилась полная инициализация данных как мы этого и хотели. Теперь, при вызове метода:

l.draw()

мы видим отображение всех указанных локальных свойств. И иерархия наших классов стала следующей:

Но при таком построении программы есть один большой недостаток. Если, например, мы в классе Line поменяем порядок наследования базовых классов:

class Line(Styles, Pos):

то при выполнении у нас возникнет ошибка, т.к. сначала будет вызван конструктор класса Styles, а в нем по прежнему всего два параметра. Ну, хорошо, давайте добавим и ему остаточные аргументы:

class Styles:
    def __init__(self, color, width, *args):
        print("Конструктор Styles")
        self._color = color
        self._width = width
        Pos.__init__(self, *args)

И, далее, мы должны, получается, еще вызвать конструктор класса Pos. Но, тогда у нас получится рекурсия: из Styles вызываем Pos, а из Pos – Styles! И такая путаница возникла всего из двух базовых классов! А представьте, если их будет десяток? Для выхода из такой ситуации как раз и была предложена функция super(). Она позволяет обойти всех предков в определенном порядке и только один раз. Давайте поставим вызов этой функции вместо конкретных классов:

super().__init__(*args)

И, теперь, при запуске у нас нет никаких ошибок, кроме изменения порядка записи значений в атрибуты. Но, это логично, т.к. мы сначала вызвали Styles и записали координаты, а затем, Pos и записали через него свойства. Если поменять порядок наследования:

class Line(Pos, Styles):

то все будет в правильном порядке.

Но такой вариант множественного наследования тоже не очень хорош. Вызывать конструкторы с аргументами в этом случае порочная практика, т.к. даже незначительное изменение иерархии классов может привести к серьезным ошибкам. Поэтому, чаще всего, конструкторы при множественном наследовании записывают без аргументов. В нашем случае программу можно очень легко изменить, записав ее так. В класс Line добавим конструктор с аргументами:

def __init__(self, sp:Point, ep:Point, color = "red", width =1):
    self._sp = sp
    self._ep = ep
    self._color = color
    self._width = width
    super().__init__()

И, далее, просто вызываем super() и указываем конструкторы вышестоящих классов без аргументов:

class Styles:
    def __init__(self):
        print("Конструктор Styles")
        super().__init__()
 
class Pos:
    def __init__(self):
        print("Конструктор Pos")
        super().__init__()

Запускаем программу, и видим, что все работает абсолютно также. Но, зато теперь, мы можем наследовать классы в любом порядке:

class Line(Styles, Pos):

И это не приведет к какой-либо ошибке. Кстати, в действительности, на вершине иерархии наших классов стоит класс object, который наследуется по умолчанию любым классом в Python:

Так вот, функция super() перебирает предков в соответствии с алгоритмом поиска C3:

C3 – поиск в дереве наследования классов Python 3 (MRO)

Очень упрощенно его можно представить так:

  • формируется список родителей в порядке их наследования
  • в конец этого списка добавляются родители этих родителей (в том же порядке (и т.д.)
  • если класс встречается второй раз, то он не заносится в список.

Более подробно об этом алгоритме можно почитать здесь:

https://ru.wikipedia.org/wiki/C3-линеаризация

В соответствии с алгоритмом MRO обход нашего дерева наследования будет происходить так:

Line → Pos → Styles → object

Кстати, для любого класса мы можем посмотреть этот список:

print( Line.__mro__ )

И получаем последовательность в соответствии с описанным алгоритмом.

Задание для самоподготовки

Создайте дочерний класс Motherboard (материнская плата), которая наследуется от классов: CPU (процессор), GPU (графич. сопроцессор), Memory (память). В свою очередь CPU наследуется от классов: AMD и Intell, GPU от классов NVidia, GeForce.

Создайте экземпляр класса Motherboard и наполните ее конкретным содержимым (локальным свойствам этого объекта присвойте определенные значения). Определите вспомогательные методы в базовых классах и выведите итоговую информацию в консоль с помощью метода showInfo() класса Motherboard.