Множественное наследование
Множественное наследование — это механизм, при котором класс может наследовать сразу от нескольких родительских классов. Это позволяет объединять функциональность из разных классов, создавая более универсальные и гибкие структуры.
Синтаксис
class Child(Parent1, Parent2):
pass
Child— дочерний класс.Parent1,Parent2— родительские классы, перечисленные через запятую.
class Printable:
def print_info(self) -> None:
print("Печать информации...")
class Savable:
def save(self) -> None:
print("Сохраняем в файл...")
class Report(Printable, Savable):
pass
report = Report()
report.print_info() # унаследовано от Printable
report.save() # унаследовано от Savable
Особенности
- Все методы и атрибуты родительских классов доступны в дочернем.
- Порядок перечисления родителей имеет значение — используется слева направо.
- Если имена методов совпадают, будет вызван метод первого подходящего родителя.
Функция hasattr
hasattr(obj, name) — это встроенная функция Python, которая позволяет проверить наличие атрибута у объекта. Возвращает True, если атрибут существует, и False, если отсутствует.
class User:
def __init__(self, username: str) -> None:
self.username = username
user = User("Alice")
print(hasattr(user, "username")) # True — атрибут существует
print(hasattr(user, "email")) # False — атрибута нет
Когда использовать
- В миксинах и декораторах, чтобы понять, поддерживает ли объект нужное поведение.
- При динамическом добавлении полей.
- В системах, где объекты приходят из разных источников и не имеют стабильной структуры.
Миксины
Миксин — это вспомогательный класс, который добавляет дополнительное поведение другим классам через множественное наследование. Миксины не предназначены для самостоятельного использования: они не создают объекты, а лишь «примешиваются» к другим классам.
Признаки миксинов
- Содержат одну или несколько полезных функций.
- Не имеют собственного состояния (
__init__обычно не определяют). - Используются только вместе с другими классами.
- Имя обычно заканчивается на
Mixin(например,LoggableMixin,SavableMixin).
Польза миксинов
- Упрощают структуру классов, разделяя обязанности.
- Позволяют повторно использовать поведение без дублирования кода.
- Делают код гибким и расширяемым при помощи множественного наследования.
class AuthMixin:
def login(self) -> None:
if not hasattr(self, "username"):
raise AttributeError("Не задан username")
print(f"{self.username} вошёл в систему.")
def logout(self) -> None:
print("Пользователь вышел из системы.")
class NotificationMixin:
def send_email(self, message: str) -> None:
if not hasattr(self, "email"):
raise AttributeError("Не задан email")
print(f"Отправка письма на {self.email}: {message}")
class UserProfile(AuthMixin, NotificationMixin):
def __init__(self, username: str, email: str) -> None:
self.username = username
self.email = email
user = UserProfile("alice", "alice@example.com")
user.login() # alice вошёл в систему.
user.send_email("Добро пожаловать!") # Отправка письма на alice@example.com: Добро пожаловать!
user.logout() # Пользователь вышел из системы.
hasattr описана в разделе Built-in Functions документации Python.
Порядок поиска методов при наследовании
Когда у объекта вызывается метод, Python ищет его:
- В самом классе.
- В первом родителе (слева направо).
- Во всей его иерархии вглубь, от наследника к предкам.
- Далее во втором родителе и его иерархии.
- И так далее, слева направо, по всем родителям.
- Последним проверяется базовый класс
object.
Как только метод найден — поиск прекращается.
class A:
def greet(self) -> None:
print("Hello from A")
class B(A):
pass
class C:
def greet(self) -> None:
print("Hello from C")
class D(B, C):
pass
d = D() # D и B не имеют метода greet
d.greet() # Hello from A
Как ищется метод greet:
D— нет.B— нет.A— нашёл! Вызывается.- До
CPython уже не доходит.
Diamond problem — проблема ромба
Это ситуация в множественном наследовании, когда класс наследует от нескольких классов, которые в свою очередь происходят от одного общего предка.
A
/ \
B C
\ /
D
Порядок разрешения методов (MRO)
Чтобы определить однозначный порядок поиска, Python использует механизм MRO (Method Resolution Order) — порядок разрешения методов.
Что делает MRO
- Определяет, в каком порядке искать методы и поля.
- Работает автоматически при вызове любого метода.
- Учитывает весь путь наследования, включая множественное.
Как посмотреть MRO
print(ClassName.__mro__) # возвращает кортеж классов
class A:
def greet(self) -> None:
print("Hello from A")
class B(A):
def greet(self) -> None:
print("Hello from B")
class C(A):
def greet(self) -> None:
print("Hello from C")
class D(B, C):
def greet(self) -> None:
print("Hello from D")
print(D.__mro__)
# (<class __main__.D>, <class __main__.B>, <class __main__.C>, <class __main__.A>, <class object>)
Функция super в множественном наследовании
В множественном наследовании функция super() играет особенно важную роль: она позволяет гарантированно вызывать следующий метод в цепочке MRO, а не просто обращаться к конкретному родителю.
ParentClass.method(self) # плохо — жёсткая привязка, не учитывает MRO
super().method() # хорошо — учитывает порядок разрешения методов
super()— это следующий класс в цепочке MRO, у которого есть метод.- Такой подход делает код гибким, надёжным и расширяемым, особенно в системах с несколькими родителями.
class A:
def action(self) -> None:
print("A")
class B(A):
def action(self) -> None:
print("B")
super().action()
class C(A):
def action(self) -> None:
print("C")
super().action()
class D(B, C):
def action(self) -> None:
print("D")
super().action()
d = D()
d.action()
# D
# B
# C
# A
Что происходит: метод action вызывается в порядке MRO, переходя через super(). Таким образом выполняются все реализации метода, а не только одна.
super() не всегда означает «ближайший родитель». Это следующий класс в MRO, у которого есть нужный метод. В множественном наследовании следующий класс может быть не тем, кого вы ожидаете.
Композиция и агрегация
Композиция и агрегация — это два способа построения отношений между объектами, при которых один объект включает в себя другой как часть своей структуры. Они оба позволяют собирать сложные объекты из более простых, но различаются по уровню зависимости.
Композиция
Композиция — отношения между объектами, когда один объект полностью принадлежит другому, то есть является частью его композиции.
- Один объект владеет другим и отвечает за его создание и удаление.
- Связь очень тесная: если уничтожить внешний объект — внутренний тоже исчезнет.
- Вложенный объект не существует отдельно.
class ExitButton:
def click(self) -> None:
print("Выход из программы")
class Menu:
def __init__(self) -> None:
self.exit_button = ExitButton() # создаётся внутри
def show(self) -> None:
print("Меню открыто")
self.exit_button.click()
menu = Menu()
menu.show()
Агрегация
Агрегация — отношения между объектами, когда один объект использует другой, но не управляет его созданием или удалением. Связанный объект создаётся вне основного и может использоваться в других местах.
- Объект использует другой, но не владеет им напрямую.
- Вложенный объект создаётся снаружи и передаётся при инициализации.
- Если уничтожить внешний объект — вложенный может продолжать жить.
class University:
def __init__(self, name: str) -> None:
self.name = name
def get_info(self) -> None:
print(f"Обучение проходит в университете: {self.name}")
class Teacher:
def __init__(self, name: str, university: University) -> None:
self.name = name
self.university = university # передаётся извне
def introduce(self) -> None:
print(f"Преподаватель: {self.name}")
self.university.get_info()
uni = University("Tech University")
t1 = Teacher("Anna", uni)
t2 = Teacher("Dmitry", uni)
t1.introduce()
t2.introduce()
Вложенные классы как композиция
Иногда один класс логически является частью другого и не имеет смысла вне его контекста. В таких случаях удобно определять вспомогательный класс внутри внешнего. Это тоже считается композицией.
class Smartphone:
class Battery:
def __init__(self, capacity: int) -> None:
self.capacity = capacity
self.charge = capacity
def use(self, amount: int) -> None:
self.charge = max(self.charge - amount, 0)
print(f"Батарея: {self.charge}/{self.capacity} мАч")
def __init__(self, model: str, battery_capacity: int) -> None:
self.model = model
self.battery = self.Battery(battery_capacity)
def play_video(self) -> None:
print(f"{self.model} воспроизводит видео...")
self.battery.use(300)
phone = Smartphone("Pixel 9", 4000)
phone.play_video()
Главные различия
| Композиция | Агрегация |
|---|---|
| Объект создаёт внутренний сам | Объект получает готовый элемент извне |
| Жёсткая связь, вложенный неотделим | Слабая связь, вложенный может существовать отдельно |
| Жизненный цикл управляется внешним объектом | Жизненный цикл независимый |
Car создаёт Engine внутри себя | Car получает Engine как аргумент |
Самая простая проверка — посмотреть на правую часть присваивания в __init__:
self.part = Part() # композиция: объект создаёт свою часть сам
self.part = ready_part # агрегация: объект использует готовую часть извне
Отсюда следует и разница во времени жизни: двигатель исчезает вместе с машиной, а водитель — нет, он существовал до неё и может пересесть в другую.
super() см. Python Tutorial — Multiple Inheritance и super().