Множественное наследование и MRO
До этого урока класс всегда имел одного родителя. Но Python позволяет наследоваться сразу от нескольких классов. Это мощная возможность — и одновременно источник одной из самых известных проблем в ООП. Разберёмся, как с этим жить.
📐 Синтаксис: class C(A, B)
Множественное наследование записывается точно так же, как обычное, только в скобках перечисляем несколько классов через запятую. Дочерний класс получает атрибуты и методы от каждого из родителей.
class A:
def hello(self):
return "Привет от A"
def info(self):
return "Я — класс A"
class B:
def bye(self):
return "Пока от B"
def info(self):
return "Я — класс B"
class C(A, B): # ← наследуем от A и B сразу
pass
obj = C()
print(obj.hello()) # Привет от A (метод из A)
print(obj.bye()) # Пока от B (метод из B)
print(obj.info()) # Я — класс A (из A, потому что A стоит первым!)
C(A, B) — победит A. Но это только вершина айсберга — под ней скрывается MRO, о котором поговорим чуть позже.
🧩 Когда это полезно: паттерн «Миксин»
На практике множественное наследование используется в основном для миксинов (Mixin). Слово происходит от английского «mix in» — примешивать, добавлять.
Миксин — это небольшой вспомогательный класс, который выполняет одну конкретную задачу и предназначен для «добавки» к другим классам. Он не используется самостоятельно — только как второй (или третий) родитель.
🍕 Аналогия из жизни: представь, что ты заказываешь пиццу. Основа — сам класс. Начинки — миксины. Ты можешь взять любую комбинацию начинок, не переделывая саму пиццу. Хочешь «логирование» — добавь LoggerMixin. Хочешь «экспорт в JSON» — добавь JsonMixin. Каждая начинка делает одно дело и делает его хорошо.
Давай посмотрим на реальный пример. Создадим LoggerMixin — миксин, который добавляет любому классу метод log():
class LoggerMixin:
"""Миксин: добавляет метод log() к любому классу."""
def log(self, message):
class_name = self.__class__.__name__
print(f"[LOG] {class_name}: {message}")
# Используем миксин в разных классах
class User(LoggerMixin):
def __init__(self, name):
self.name = name
def login(self):
self.log(f"Пользователь {self.name} вошёл в систему")
class Order(LoggerMixin):
def __init__(self, order_id):
self.order_id = order_id
def create(self):
self.log(f"Создан заказ #{self.order_id}")
user = User("Алиса")
user.login() # [LOG] User: Пользователь Алиса вошла в систему
order = Order(42)
order.create() # [LOG] Order: Создан заказ #42
log() написан один раз, а используется в десятках классов. Изменил логику в LoggerMixin — изменение автоматически применилось везде. Это называется принципом «не повторяй себя» (DRY — Don't Repeat Yourself).
💎 Проблема ромба (Diamond Problem)
А теперь — та самая знаменитая проблема. Представь такую иерархию классов:
A
/ \
B C
\ /
D
Класс A — прадед. Классы B и C оба наследуются от A. Класс D наследуется от B и C. Получается форма ромба — отсюда и название.
Теперь вопрос: если у A, B и C есть одинаковый метод who() — чей метод получит D? В некоторых языках (например, в C++) это была настоящая головная боль. Смотрим на код:
class A:
def who(self):
return "Я — A"
class B(A):
def who(self):
return "Я — B"
class C(A):
def who(self):
return "Я — C"
class D(B, C): # ← наследуем от B и C (оба от A — ромб!)
pass
d = D()
print(d.who()) # Кто победит: A, B или C?
В Python ответ чёткий и предсказуемый: победит B. Почему именно B, а не C? Потому что Python использует специальный алгоритм для определения порядка — MRO.
🗺️ MRO — порядок поиска методов
MRO расшифровывается как Method Resolution Order — «порядок разрешения методов». Это заранее составленный список: в каком порядке Python обходит классы в поисках нужного метода.
Когда ты вызываешь метод у объекта, Python не угадывает — он идёт по этому списку строго слева направо и берёт первый класс, у которого такой метод есть.
Посмотреть MRO конкретного класса можно двумя способами:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
# Способ 1: атрибут __mro__ — возвращает кортеж классов
print(D.__mro__)
# (, , ,
# , )
# Способ 2: метод mro() — возвращает список
print(D.mro())
# [, , ,
# , ]
D → потом в B → потом в C → потом в A → потом в базовом object. Вот почему для ромба победил B: он стоит раньше C в порядке обхода. И заметь — A встречается только один раз, в самом конце. Это ключевое свойство MRO.
⚙️ Алгоритм C3 — как Python строит MRO
Python использует алгоритм C3 Linearization (C3 линеаризация) для вычисления MRO. Не нужно знать математику за ним — важно понять три правила, которым он следует:
Поиск всегда начинается с самого класса, у которого вызван метод.
Родители обходятся в том порядке, в котором ты их написал в скобках: class D(B, C) → сначала B, потом C.
Общий предок (A) появляется в MRO только после всех его потомков. Никаких дублей.
TypeError: Cannot create a consistent method resolution order (MRO). Это сигнал: ты запутал иерархию классов и нужно её переосмыслить.
🔗 super() идёт по MRO
В предыдущем уроке мы познакомились с super(). В контексте множественного наследования super() — это не просто «вызов родителя». Точнее сказать: super() вызывает следующий класс в MRO.
Это значит, что при правильном использовании super() все классы в цепочке получат возможность выполнить свой код. Смотрим:
class A:
def __init__(self):
print("A.__init__ вызван")
super().__init__() # идёт дальше по MRO → object
class B(A):
def __init__(self):
print("B.__init__ вызван")
super().__init__() # по MRO следующий — C (не A!)
class C(A):
def __init__(self):
print("C.__init__ вызван")
super().__init__() # по MRO следующий — A
class D(B, C):
def __init__(self):
print("D.__init__ вызван")
super().__init__() # по MRO следующий — B
# MRO для D: D → B → C → A → object
d = D()
# D.__init__ вызван
# B.__init__ вызван
# C.__init__ вызван
# A.__init__ вызван
print(D.mro())
# [D, B, C, A, object]
B.__init__ вызвал super(), следующим оказался не A (прямой родитель B), а C — потому что MRO для D такой: D → B → C → A → object. super() всегда смотрит на MRO текущего объекта, а не на иерархию текущего класса. Это неочевидно, но именно поэтому цепочка работает правильно.
🚫 Когда НЕ стоит использовать множественное наследование
Короткий ответ: почти всегда. Множественное наследование — мощный инструмент, но он легко превращается в запутанный код, в котором сложно понять, откуда пришёл тот или иной метод.
Наследование от двух «полноценных» классов с похожими методами — class Duck(Bird, Fish). MRO запутает тебя и коллег.
Глубокие ромбовидные иерархии из 4+ уровней. Никто не сможет понять, чей метод вызывается.
Миксины с одной ответственностью: class User(LoggerMixin, JsonMixin, Base). Миксины маленькие, не пересекаются, их роль очевидна.
🎯 Полный демо: несколько миксинов
Посмотрим на реалистичный пример: добавляем к классам сразу два миксина — логирование и сериализацию в словарь.
import datetime
class LoggerMixin:
"""Миксин: добавляет метод log() для вывода сообщений."""
def log(self, message):
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
class_name = self.__class__.__name__
print(f"[{timestamp}] {class_name}: {message}")
class DictMixin:
"""Миксин: добавляет метод to_dict() — конвертация в словарь."""
def to_dict(self):
return {
key: value
for key, value in self.__dict__.items()
if not key.startswith("_")
}
# Основные бизнес-классы используют миксины
class Product(LoggerMixin, DictMixin):
def __init__(self, name, price):
self.name = name
self.price = price
def apply_discount(self, percent):
old_price = self.price
self.price = self.price * (1 - percent / 100)
self.log(f"Скидка {percent}%: {old_price} → {self.price:.2f}")
class Customer(LoggerMixin, DictMixin):
def __init__(self, username, email):
self.username = username
self.email = email
def update_email(self, new_email):
self.email = new_email
self.log(f"Email обновлён: {new_email}")
# Демонстрация
product = Product("Ноутбук", 80000)
product.apply_discount(15)
# [14:32:10] Product: Скидка 15%: 80000 → 68000.00
print(product.to_dict())
# {'name': 'Ноутбук', 'price': 68000.0}
customer = Customer("alice", "alice@example.com")
customer.update_email("newalice@example.com")
# [14:32:10] Customer: Email обновлён: newalice@example.com
print(customer.to_dict())
# {'username': 'alice', 'email': 'newalice@example.com'}
# Смотрим MRO
print(Product.mro())
# [Product, LoggerMixin, DictMixin, object]
LoggerMixin и DictMixin — независимые небольшие классы. Они не знают друг о друге и не знают о бизнес-классах. Их можно добавлять к любому классу одной строкой. MRO простой и линейный — никаких ромбов.
📌 Итоги урока
- Множественное наследование —
class C(A, B)— дочерний класс получает методы от всех родителей. - Миксин — небольшой класс с одной задачей, предназначенный для «добавки» к другим классам.
- Diamond Problem — когда два родителя имеют общего предка, возникает ромбовидная иерархия. Python решает это через MRO.
- MRO (Method Resolution Order) — заранее вычисленный порядок обхода классов. Смотреть через
ClassName.__mro__илиClassName.mro(). - Алгоритм C3 гарантирует: каждый класс встречается в MRO ровно один раз, порядок родителей соблюдается, предки идут после потомков.
super()в множественном наследовании идёт по MRO, а не «к прямому родителю».- Множественное наследование стоит использовать только для миксинов. В остальных случаях предпочти композицию.