📖 Теория: множественное наследование

⚡ Кратко

Класс может наследовать сразу от нескольких родителей: class Child(Parent1, Parent2):. Порядок родителей влияет на выбор метода при совпадающих именах. Python ищет методы слева направо и вглубь, а потом — по общему предку. MRO можно посмотреть через Class.__mro__. super() вызывает следующий метод в этой цепочке. Миксины — это маленькие вспомогательные классы с поведением. Композиция и агрегация позволяют собирать объекты из частей без наследования.

Множественное наследование

Множественное наследование — это механизм, при котором класс может наследовать сразу от нескольких родительских классов. Это позволяет объединять функциональность из разных классов, создавая более универсальные и гибкие структуры.

Синтаксис

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 ищет его:

  1. В самом классе.
  2. В первом родителе (слева направо).
  3. Во всей его иерархии вглубь, от наследника к предкам.
  4. Далее во втором родителе и его иерархии.
  5. И так далее, слева направо, по всем родителям.
  6. Последним проверяется базовый класс 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:

  1. D — нет.
  2. B — нет.
  3. A — нашёл! Вызывается.
  4. До C Python уже не доходит.

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   # агрегация: объект использует готовую часть извне

Отсюда следует и разница во времени жизни: двигатель исчезает вместе с машиной, а водитель — нет, он существовал до неё и может пересесть в другую.

Проверить по документации: подробнее о множественном наследовании, MRO и super() см. Python Tutorial — Multiple Inheritance и super().