Множественное наследование в Python
Множественное наследование — это возможность класса иметь более одного родительского класса.
При множественном наследовании дочерний класс наследует все свойства родительских классов. Синтаксис множественного наследования очень похож на синтаксис обычного наследования.
class Base1: pass class Base2: pass # этот класс наследует сразу от двух родительских классов: Base1 и Base2 class MultiDerived(Base1, Base2): pass
Многоуровневое наследование
Мы также можем наследовать класс от уже наследуемого. Это называется многоуровневым наследованием. Оно может иметь сколько угодно уровней.
В многоуровневом наследовании свойства родительского класса и наследуемого от него класса передаются новому наследуемому классу.
class Base: pass class Derived1(Base): pass class Derived2(Derived1): pass
Класс Derived1 наследуется от класса Base , а класс Derived2 — от класса Derived1 .
Порядок разрешения методов (MRO)
Все классы в Python наследуются от класса object . Это базовый класс языка.
И поэтому технически все классы, встроенные или определенные пользователем, являются наследуемыми, а все объекты — экземплярами класса object .
# Вывод: True print(issubclass(list,object)) # Вывод: True print(isinstance(5.5,object)) # Вывод: True print(isinstance("Привет",object))
По порядку разрешения методов любой указанный атрибут сначала ищется в объявленном классе. Если его там нет, поиск продолжается в родительских классах на максимальную глубину слева направо без прохода по одному классу дважды.
Например, в случае с классом MultiDerived порядок поиска будет следующим: [MultiDerived, Base1, Base2, object]. Такой порядок еще называется линеаризацией класса MultiDerived, а список правил, по которому мы находим такой порядок, называется Method Resolution Order (порядок разрешения методов).
MRO должен сохранять локальный порядок старшинства, а также обеспечивать монотонность. Он гарантирует, что класс всегда будет появляться до родителей. В случае нескольких родителей порядок будет таким же, как у кортежей в базовом классе.
MRO класса можно просмотреть в атрибуте __mro__ или с помощью метода mro() . Вызов атрибута возвращает кортеж, а вызов метода — список.
>>> MultiDerived.__mro__ (, , , ) >>> MultiDerived.mro() [, , , ]
Еще более сложный пример множественного наследования и его визуализация по MRO:
# Как работает MRO class X: pass class Y: pass class Z: pass class A(X, Y): pass class B(Y, Z): pass class M(B, A, Z): pass # Вывод: # [, , # , , # , , # ] print(M.mro())
Множественное наследование
Мы продолжаем изучать тему наследования. В языке Python допускается множественное наследование, когда один дочерний класс образуется сразу от нескольких базовых, согласно синтаксису:
class A(base1, base2, …, baseN):
…
Давайте посмотрим, как это работает и для чего вообще нужно. Признаюсь честно, я не сразу смог придумать учебный пример, где можно было бы показать целесообразность применения множественного наследования. То есть, он применяется не так часто, как обычное наследование от одного класса. Но, тем не менее это тоже важный механизм и некоторые подходы к программированию его активно используют. Например, идея миксинов (mixins) в Python реализуется через множественное наследование. И учебный пример я решил связать именно с ними, а заодно показать еще один прием в программировании.
class Goods: def __init__(self, name, weight, price): print("init MixinLog") self.name = name self.weight = weight self.price = price def print_info(self): print(f", , ")
Пока ничего нового здесь нет. Но потом, к нам подходит тимлид и говорит: — Дорогой сеньор, добавь, пожалуйста, возможность логирования товаров магазина. И как бы вы поступили на месте этого сеньора? Плохой сеньор начнет прописывать логику логирования либо непосредственно в базовом классе Goods, либо уровнем выше (в иерархии наследования). А хороший воспользуется идеей миксинов. Для этого он создаст еще один класс, который можно назвать:
class MixinLog: ID = 0 def __init__(self): print("init MixinLog") self.ID += 1 self.id = self.ID def save_sell_log(self): print(f": товар продан в 00:00 часов")
Этот класс работает совершенно независимо от классов Goods и Notebook и лишь добавляет функционал по логированию товаров с использованием их id. Такие независимые базовые классы и получили название миксинов – примесей. Добавим этот класс в цепочку наследования:
class NoteBook(Goods, MixinLog): pass
И видим ошибку. Очевидно, она связана с тем, что у второго класса MixinLog не был вызван инициализатор. Почему так произошло? Как мы уже знаем, при создании объектов инициализатор ищется сначала в дочернем классе, но так как его там нет, то в первом базовом Goods. Он там есть, выполняется и на этом инициализация нашего объекта NoteBook завершается. Однако, нам нужно также взывать инициализатор и второго базового класса MixinLog. В данном случае, сделать это можно с помощью объекта-посредника super(), которая и делегирует вызов метода __init__ класса MixinLog:
class Goods: def __init__(self, name, weight, price): super().__init__() print("init Goods") self.name = name self.weight = weight self.price = price …
Теперь, после запуска программы, мы видим, что оба инициализатора сработали и ошибок никаких нет. Но откуда функция super() «знает», что нужно обратиться ко второму базовому классу MixinLog, а, скажем, не к базовому классу object, от которого неявно наследуются все классы верхнего уровня? В Python существует специальный алгоритм обхода базовых классов при множественном наследовании. Сокращенно, он называется: MRO – Method Resolution Order И говорит, в каком порядке обходить базовые классы: Мы можем увидеть эту цепочку обхода базовых классов, если распечатать специальную коллекцию __mro__ любого класса:
В консоли появится следующая последовательность: (
class NoteBook(MixinLog, Goods): pass
И мы сразу получаем ошибку, что в метод __init__ передаются четыре аргумента, а он принимает только один, так как здесь отрабатывает инициализатор уже класса MixinLog. Так что порядок следования базовых классов при множественном наследовании имеет важное значение. Первым должен идти «основной» класс и у него, как правило, инициализатор имеет несколько параметров. А далее, записываются классы, у которых, опять же, как правило, инициализаторы имеют только параметр self. Это второй важный момент. Когда мы собираемся использовать множественное наследование, то структуру классов следует продумывать так, чтобы инициализаторы вспомогательных классов имели только один параметр self, иначе будут сложности их использования. В чем они состоят? Давайте для примера пропишем в инициализаторе класса MixinLog один параметр p1
class MixinLog: def __init__(self, p1): super().__init__(1, 2) …
class MixinLog2: def __init__(self, p1, p2): super().__init__() print("init MixinLog 2")
В каждом методе __init__ мы также делаем делегированный вызов инициализатора следующего базового класса. А цепочка наследования будет такой:
class NoteBook(Goods, MixinLog, MixinLog2): pass
Сейчас при запуске у нас не возникает никаких ошибок, так как последовательность MRO имеет вид: (
class NoteBook(Goods, MixinLog2, MixinLog): pass
то все нарушится и получим ошибки. Чтобы в программах при множественном наследовании не возникало проблем с зависимостью последовательности наследования дополнительных базовых классов, их инициализаторы следует создавать с одним параметром self и в каждом из них прописывать делегированный вызов инициализатора следующего класса командой:
Тогда точно никаких особых проблем при использовании множественного наследования не возникнет. Последнее, что я хочу отметить на этом занятии, это вызов методов с одинаковыми именами из базовых классов. Давайте предположим, что в классе MixinLog имеется метод print_info с тем же именем, что и в классе Goods:
def print_info(self): print("print_info класса MixinLog")
то мы обратимся к методу класса Goods, так как он записан первым в цепочке наследования и в соответствии с алгоритмом обхода MRO он будет найден первым. Но что если мы хотим вызвать этот метод из второго базового класса MixinLog? Как поступить? Сделать это можно двумя способами. Либо напрямую вызвать этот метод через класс MixinLog:
Обратите внимание, что в этом случае нам обязательно нужно указать первым аргументом ссылку на объект класса NoteBook. Либо, определить какой-либо метод в классе NoteBook (пусть он называется также):
class NoteBook(Goods, MixinLog): def print_info(self): MixinLog.print_info(self)