Множественное и многоуровневое наследование в Python
Множественное наследование в Python позволяет классу получить атрибуты и методы сразу от нескольких родителей. В статье разобраны синтаксис, отличие от многоуровневого наследования, порядок поиска методов MRO, ромбовидная схема и правильная работа super(). Отдельно показаны миксины, ради которых этот механизм чаще всего и используют.

Содержание статьи
Синтаксис множественного наследования
Родительские классы перечисляют через запятую в скобках после имени класса. Наследник получает все методы и атрибуты каждого родителя, а при совпадении имен побеждает тот родитель, который указан левее. Основы одиночного наследования описаны в статье про наследование классов, здесь речь о случае с несколькими предками.
class Flyer:
def fly(self):
return f"{self.name} летит"
def move(self):
return "по воздуху"
class Swimmer:
def swim(self):
return f"{self.name} плывет"
def move(self):
return "по воде"
class Duck(Flyer, Swimmer):
def __init__(self, name):
self.name = name
d = Duck("Утка")
print(d.fly()) # Утка летит
print(d.swim()) # Утка плывет
print(d.move()) # по воздуху: Flyer стоит первымПроверка через isinstance(d, Swimmer) вернет True: объект считается экземпляром каждого из родителей. Это позволяет передавать утку в любую функцию, которая ждет летающий или плавающий объект.
Многоуровневое наследование
Многоуровневое наследование - это цепочка: класс наследует от наследника другого класса. Глубина цепочки не ограничена, а методы ищутся снизу вверх, от самого производного класса к базовому.
class Base:
def info(self):
return "Base"
class Derived1(Base):
pass
class Derived2(Derived1):
def info(self):
return "Derived2 и " + super().info()
print(Derived2().info()) # Derived2 и BaseУ Derived1 нет своего метода info(), поэтому super() из Derived2 дойдет до Base. Оба вида наследования легко сочетаются в одной иерархии.
| Вид | Как записывается | Сколько прямых родителей |
|---|---|---|
| Одиночное | class B(A) | Один |
| Многоуровневое | class C(B), где B(A) | Один, но у него свои предки |
| Множественное | class D(B, C) | Два и больше |
Порядок поиска методов MRO
Когда у нескольких предков есть метод с одним именем, Python выбирает его по списку MRO (Method Resolution Order). Список строится алгоритмом C3-линеаризации и подчиняется двум правилам: потомок всегда идет раньше своих родителей, а родители идут в том порядке, в каком перечислены в скобках. Посмотреть список можно через атрибут __mro__ или метод mro().
print([cls.__name__ for cls in Duck.__mro__])
# ['Duck', 'Flyer', 'Swimmer', 'object']Если порядок родителей противоречив и построить согласованный список нельзя, класс не создастся вовсе. Например, запись class X(Base, Derived1) вызовет TypeError: Cannot create a consistent method resolution order (MRO), потому что Base указан раньше своего же наследника.
Ромбовидное наследование и super()
Ромб возникает, когда два родителя класса наследуют от общего предка. Главный вопрос в такой схеме: сколько раз вызовется метод общего предка. Благодаря MRO и super() он вызывается ровно один раз.
class A:
def hello(self):
print("A")
class B(A):
def hello(self):
print("B")
super().hello()
class C(A):
def hello(self):
print("C")
super().hello()
class D(B, C):
def hello(self):
print("D")
super().hello()
D().hello() # D B C A (каждое с новой строки)
print([k.__name__ for k in D.mro()]) # ['D', 'B', 'C', 'A', 'object']Ключевой момент: super() вызывает не родителя текущего класса, а следующий класс в MRO того объекта, с которым работает метод. Внутри B.hello() для объекта D следующим идет C, а не A. Если бы B вызывал A.hello(self) напрямую по имени, метод C был бы пропущен.
Кооперативный __init__ с несколькими родителями
Чтобы инициализация прошла по всей цепочке, каждый класс должен вызывать super().__init__() и передавать дальше аргументы, которые ему не нужны. Для этого используют **kwargs: каждый класс забирает свои именованные аргументы, а остальное отдает следующему.
class Named:
def __init__(self, name, **kwargs):
super().__init__(**kwargs)
self.name = name
class Aged:
def __init__(self, age, **kwargs):
super().__init__(**kwargs)
self.age = age
class Person(Named, Aged):
pass
p = Person(name="Оля", age=25)
print(p.name, p.age) # Оля 25Цепочка заканчивается на object.__init__(), который не принимает аргументов. Поэтому к этому моменту все kwargs должны быть разобраны, иначе вы получите TypeError.
Миксины: практическое применение
Миксин (mixin) - небольшой класс, который добавляет одну возможность и не предназначен для создания собственных объектов. Такие классы подмешивают к основному через множественное наследование. Подход широко используется в Django и стандартной библиотеке, например в socketserver.ThreadingMixIn.
import json
class ToJsonMixin:
def to_json(self):
return json.dumps(self.__dict__, ensure_ascii=False)
class ReprMixin:
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{type(self).__name__}({fields})"
class User(ToJsonMixin, ReprMixin):
def __init__(self, login, email):
self.login = login
self.email = email
u = User("admin", "a@example.com")
print(u.to_json()) # {"login": "admin", "email": "a@example.com"}
print(u) # User(login='admin', email='a@example.com')Миксины принято ставить левее основного базового класса: class MyView(LoginRequiredMixin, View). Тогда их методы перекрывают методы базы и могут вызвать ее реализацию через super(). Подробнее о методах вроде __repr__ рассказано в статье про магические методы.
Когда лучше обойтись без множественного наследования
Если классы-родители большие, со своим состоянием и пересекающимися именами атрибутов, иерархия быстро становится запутанной. Признак проблемы: чтобы понять поведение метода, нужно выписывать MRO. В таких случаях чаще подходит композиция, когда объект хранит другой объект в атрибуте и вызывает его методы. Отношение "имеет" (у машины есть двигатель) выражают композицией, а "является" (утка является птицей) наследованием. Общие принципы ООП в Python собраны в статье ООП Python, а формальное описание алгоритма есть в документации Python.
Частые вопросы
Ответы на вопросы, которые чаще всего возникают при изучении темы.
Есть ли в Python интерфейсы, как в Java?
Отдельного ключевого слова нет. Их роль играют абстрактные классы из модуля abc и протоколы typing.Protocol. Наследовать от нескольких абстрактных классов можно так же, как от обычных.
Как узнать, от каких классов наследует класс?
Атрибут __bases__ содержит кортеж прямых родителей, а __mro__ полный порядок поиска. Для проверки отношения есть функция issubclass(Duck, Flyer).
Можно ли вызвать метод конкретного родителя, минуя MRO?
Да, явным вызовом Swimmer.move(self). Это допустимо в простых случаях, но в ромбовидных иерархиях может привести к повторным вызовам общего предка.
Можно ли наследовать от встроенных типов вместе с обычными классами?
Можно, например class MyDict(ReprMixin, dict). Ограничение одно: нельзя наследовать сразу от двух встроенных типов с несовместимым устройством в памяти, например от list и dict, Python выдаст TypeError.