📦 Репозиторий занятия 72

Урок 72. Множественное наследование

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции77 строк

Множественное наследование: класс сразу от нескольких родителей

less_37__multiple_inheritance/theory_01__multiple_Inheritance.md

Вводит множественное наследование, отмечая, что в отличие от Java/C#/Swift/Go/JS, Python (наряду с C++ и Ruby) его поддерживает. На примере WorkingStudent(Person, Worker, Student) показано, что уникальные методы каждого родителя (greet, work, study) наследуются без конфликтов, а метод info(), одноимённый во всех трёх родителях, вызывает вопрос — какой из вариантов будет вызван. Отдельно перечислены особенности: порядок поиска через C3-линеаризацию (MRO), конфликт имён разрешается в пользу класса, идущего первым слева, и рекомендация использовать super() для инициализации родителей.

  • class WorkingStudent(Person, Worker, Student) — три родителя сразу
  • ws.greet()/ws.work()/ws.study() — уникальные методы каждого родителя
  • ws.info() — метод с одинаковым именем есть у всех трёх родителей, вопрос «чей вызовется?»
  • MRO и C3-линеаризация — как Python разрешает диамант наследования
  • Правило: конфликт имён решается в пользу первого слева родителя
  • super() для корректной инициализации базовых классов
Показать начало файла (77 строк всего)
### **Множественное наследование**

**Множественное наследование** — это возможность класса наследоваться сразу от нескольких базовых классов.  

В семействе языков высокого уровня множественное наследование поддерживают **далеко** не все.   
Кроме `Python`, множественное наследование ещё есть в `C++`, `Ruby`.  
А вот `Java`, `C#`, `Swift`, `Go`, `JavaScript` — нет.

В Python это реализуется следующим образом:


```python
class Person:
    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"Hello, my name is {self.name}")

    def info(self):
        print(f"Метод из класса Person")


class Worker:
    def work(self):
        print("I am working!")

    def info(self):
        print(f"Метод из класса Worker")


class Student:
    def study(self):
        print("I am studying!")

    def info(self):
        print(f"Метод из класса Student")


class WorkingStudent(Person, Worker, Student):
    def __init__(self, name):
        super().__init__(name)  # Инициализация Person


ws = WorkingStudent("Alice")
ws.greet()      # метод из Person
ws.work()       # метод из Worker
ws.study()      # метод из Student
ws.info()       # Из какого класса будет унаследован этот метод?
```

Как видим, каждый из родительских классов имеет:

1. Свой собственный уникальный метод (`greet`, `work` и `study`)
2. Метод `info`: общее название, разное содержание

---

## Особенности множественного наследования

…
Проверьте себя: Какой из трёх методов info() (Person, Worker или Student) вызовет ws.info(), и почему именно в таком порядке перечислены родители в WorkingStudent(Person, Worker, Student)?
2
MarkdownРазбор концепции101 строк

Mixin: класс, дающий поведение, но не создающий собственных объектов

less_37__multiple_inheritance/theory_02__mixin.md

Определяет миксин как класс, который «воспитывает чужих детей, но не имеет своих» — примешивает функциональность через множественное наследование, обычно без собственного состояния. На примере текстового редактора показано, как EmailMixin, SaveMixin и PrintMixin независимо друг от друга добавляют send_email/save/print_doc к базовому классу EditDocument, а итоговый TextEditor(EditDocument, EmailMixin, SaveMixin, PrintMixin) комбинирует всё сразу без изменения исходного кода документа.

  • Document/EditDocument — основной класс с состоянием (name, content)
  • EmailMixin.send_email(), SaveMixin.save(), PrintMixin.print_doc() — миксины без состояния
  • class TextEditor(EditDocument, EmailMixin, SaveMixin, PrintMixin): pass
  • doc.print_doc() → doc.edit() → doc.save() → doc.send_email(...) → doc.print_doc()
  • Преимущества: повторное использование, разделение ответственности, гибкость набора функций
Показать начало файла (101 строк всего)
### Понятие Mixin

Это класс, который 
* передаёт наследникам свою функциональность,
* но не способен создавать свои собственные объекты.

***"Воспитывать чужих детей может, а иметь своих собственных — нет."***

Главная идея:

* Mixin добавляет **одну или несколько функций** к классу.
* Обычно **не хранит состояние** (атрибуты объекта), а лишь предоставляет методы.
* Используется вместе с **множественным наследованием**.

---

#### Пример

Создание текстового редактора.  
Условие: для разных клиентов нужен разный набор функционала

```python
# Основной класс
class Document:
    def __init__(self, name, content):
        self.name = name
        self.content = content


class EditDocument(Document):
    def edit(self):
        self.content += "\nТекст был отредактирован"
        return self


# ===== Возможные варианты дополнительного функционала =====
# Mixin для отправки документа по email
class EmailMixin:
    def send_email(self, recipient):
        print(f"Sending document '{self.name}' to {recipient} via email...")


# Mixin для сохранения документа на диск
class SaveMixin:
    def save(self):
        print(f"Saving document '{self.name}' to disk...")


# Mixin для печати документа
class PrintMixin:
    def print_doc(self):
        print('-' * 50,)
        print(f"Printing document '{self.name}' \n{self.content}")
        print('-' * 50, '\n')


# ======= Создаём полноценный текстовой редактор =========

class TextEditor(EditDocument, EmailMixin, SaveMixin, PrintMixin):
    """Текстовый редактор с возможностью редактирования, сохранения, печати и отправки по email"""
…
Проверьте себя: Почему можно собрать SpreadsheetEditor из тех же трёх миксинов (EmailMixin, SaveMixin, PrintMixin) без единой правки их кода — что в дизайне миксинов это обеспечивает?
3
MarkdownРазбор концепции147 строк

Diamond problem и C3-линеаризация MRO

less_37__multiple_inheritance/theory_03__diamond_problem.md

Разбирает классическую проблему ромба: D(B, C), где оба B и C наследуют от общего A, и все три класса определяют одноимённый метод show(). Показывает, что Python решает это через C3-линеаризацию: поиск идёт по родителям слева направо (D→B→C→A), при этом общий предок A проверяется только один раз, чтобы избежать дублирования. Второй пример подтверждает то же самое для __init__() с явными вызовами super() в каждом классе — порядок вызова Init D → Init B → Init C → Init A совпадает с MRO класса D.

  • Ромбическая иерархия: A ← B, C ← D(B, C), у всех есть show()
  • d.show() → 'B' (первый по MRO слева), D.__mro__ показывает полный порядок
  • Правило C3: поиск у родителей слева направо, общий предок — только один раз
  • ClassName.__mro__ — атрибут-кортеж с порядком разрешения методов
  • Второй пример: super().__init__() в каждом классе — порядок Init D, Init B, Init C, Init A

Что выводит: Для первого примера: 'B' (напечатано методом show()), затем кортеж (<class D>, <class B>, <class C>, <class A>, <class object>). Для второго примера с super() в __init__: 'Init D', 'Init B', 'Init C', 'Init A' — каждый ровно по одному разу, несмотря на общего предка A у обоих родителей.

Показать начало файла (147 строк всего)
### Что такое проблема ромба (diamond problem)?

**Diamond problem** возникает при **множественном наследовании**, когда структура классов выглядит так:

```
      A
     / \
    B   C
     \ /
      D
```
* Во всех родительских классах есть метод `show()`, но в каждом классе он имеет разный функционал
* Класс `D` наследует от `B` и `C`.
* `B` и `C` наследуют от одного класса `A`.
* Вопрос: чей именно метод `show()` унаследует дочерний класс `D`?


---

#### Пример

```python
class A:
    def show(self):
        print("A")

class B(A):
    def show(self):
        print("B")

class C(A):
    def show(self):
        print("C")

class D(B, C):
    pass

d = D()
d.show()
print(D.__mro__)
```

### Объяснение

* `D` наследует от `B` и `C`.
* Метод `show` есть у `B`, `C` и `A`.
* Python использует **C3-линеаризацию**, чтобы определить **MRO (Method Resolution Order)**.

**Вывод программы:**

```
B
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
```

### Как Python решает `diamond problem`? (Алгоритм C3 linearization)

* Сначала поиск идёт у родителей (`B` и `C`)
* Как только метод находится, его дальнейший поиск прекращается
* Порядок поиска у родителей идёт слева-направо 
…
Проверьте себя: Почему 'Init A' печатается только один раз, хотя и B, и C вызывают super().__init__(), а оба они — наследники A?
4
MarkdownРазбор концепции154 строк

Наследование, композиция и агрегация: is-a против двух вариантов has-a

less_37__multiple_inheritance/theory_04__inheritance__vs__composition_and_aggr.md

Сопоставляет три способа связывать объекты в ООП. Наследование (is-a) — Dog является Animal, переопределяя поведение. Композиция (has-a, часть целого) — Car содержит Engine, который создаётся внутри Car и не существует отдельно от него: жизненный цикл компонента полностью зависит от владельца. Агрегация (тоже has-a, но независимый компонент) — Employee хранит ссылку на Department, который существует самостоятельно, передаётся в конструктор и может использоваться несколькими Employee одновременно. Завершается сводной ASCII-схемой и таблицей всех трёх отношений.

  • Наследование: Dog(Animal) — is-a, переопределение speak()
  • Композиция: Car создаёт self.engine = Engine() в своём __init__ — часть-целое
  • Агрегация: Employee(name, department) получает Department извне — независимый компонент
  • Ключевое отличие композиции от агрегации: кто создаёт компонент и от кого зависит его жизненный цикл
  • Итоговая таблица: отношение / жизненный цикл компонента / пример
Показать начало файла (154 строк всего)
## Наследование, Композиция, Агрегация

В ООП есть несколько способов связывать объекты между собой: 
* **наследование**, 
* **композиция**
* и **агрегация**.

---

### 1. Наследование (Inheritance)

Наследование используется, когда один класс **расширяет или модифицирует поведение другого класса**.  
Оно описывает отношение `is-a` (объект `Dog` является расширенной версией объекта `Animal`):

* `Dog` наследует свойства `Animal`
* `Dog` наследует поведение `Animal`
* `Dog` может дополнять или изменять это поведение
* `Dog` всё ещё считается `Animal`, но более конкретным


#### Пример:

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Some sound"

# Наследуемся от Animal
class Dog(Animal):
    def speak(self):
        return "Woof!"

dog = Dog("Buddy")
print(dog.name)   # Buddy
print(dog.speak())  # Woof!
```

* `Dog` **является** `Animal` → отношение `is-a`.
* Подкласс наследует методы и атрибуты родителя, но может их переопределять.

---

## 2. Композиция (Composition)

Композиция описывает отношение "**состоит из**" (`has-a`) и подразумевает,  
что один объект **содержит другой как часть себя**.  
При этом компонент обычно создаётся внутри объекта и полностью принадлежит ему:

* `Car` **содержит в себе** объект `Engine`
* `Car` **использует** `Engine` для реализации части своего поведения (например, `start()`)
* `Engine` **создаётся внутри** `Car` и **не существует отдельно** от него
* `Engine` **не является** видом `Car`, но является **частью** `Car`
* Жизненный цикл `Engine` **полностью зависит от** `Car`: 
  * когда `Car` уничтожается, `Engine` тоже исчезает
* Отношение `Car`–`Engine` — это **целое–часть**

### Пример:
…
Проверьте себя: Почему связь Car–Engine названа композицией, а Employee–Department — агрегацией, если оба случая формально являются has-a?
5
MarkdownРазбор концепции132 строк

Вложенные классы: пространство имён без автоматической связи

less_37__multiple_inheritance/theory_05__nested_classes.md

Вводит вложенный класс как класс, определённый внутри другого и существующий только как его атрибут (Human.Heart недоступен как просто Heart). Подчёркивает: вложенный класс — разновидность композиции, но более строгая — его вообще нельзя создать без упоминания внешнего класса. Ключевой раздел — отсутствие неявной связи с экземпляром внешнего класса (в отличие от Java/Kotlin): попытка обратиться к self.value внешнего объекта из Inner напрямую даёт ошибку, связь нужно передавать явно через конструктор; но доступ к АТРИБУТУ КЛАССА внешнего класса (не экземпляра) из Inner работает автоматически.

  • class Human: class Heart — Heart доступен только как Human.Heart
  • Human.Heart() работает, но просто Heart() — NameError
  • Outer.Inner без явной передачи — обращение к self.value внешнего объекта падает
  • Явная передача: Inner.__init__(self, outer: Outer) — self.outer = outer
  • Атрибуты КЛАССА внешнего доступны автоматически: print(Outer.class_attr) работает без передачи
Показать начало файла (132 строк всего)
## Вложенные классы

**Вложенный класс** — это класс, определённый *внутри другого класса*.

Он НЕ существует самостоятельно, а только как **атрибут** внешнего класса.

```python
class Human:
    class Heart:
        def beat(self):
            return "Тук-тук"

h = Human()
heart = Human.Heart()
print(heart.beat())  # Тук-тук


try:
    heart2 = Heart()
except Exception as e:
    print(f'{e.__class__.__name__}: {e}')
    # NameError: name 'Heart' is not defined
```

Как видно из примера, вложенный класс невозможно вызвать без упоминания внешнего.

Вложенный класс — это разновидность композиции.  
С той только разницей, в отличие от обычной композиции,  
вложенный класс в принципе невозможно создать без внешнего.


### Схема трёх вариантов отношения объектов:

```
        НАСЛЕДОВАНИЕ               КОМПОЗИЦИЯ                            АГРЕГАЦИЯ 
          (is-a)               (has-a, часть целого)            (has-a, независимый компонент)
             ^          
             |
     +----------------+         +-----------------+         +-----------------+       +--------------+
     |      Dog       |         |       Car       |         |                 |       |              |
     +----------------+         |  +-----------+  |         |    Employee     |-------|  Department  |
             |                  |  |           |  |         |                 |       |              |
             v                  |  |  Engine   |  |         +-----------------+       +--------------+
     +----------------+         |  |           |  |         
     |     Animal     |         |  +-----------+  |
     +----------------+         +-----------------+ 

                                     КОМПОЗИЦИЯ
                                          |
                                          v
                                +-----------------+
                                |     Car         |
                                |  class Engine   |  ← вложенный класс (часть Car)
                                +-----------------+

```

Внешний класс используют для логической группировки вспомогательных структур,   
которые не имеют смысла вне контекста внешнего класса.

…
Проверьте себя: Почему обращение к Outer.class_attr из метода Inner работает без явной передачи, а обращение к self.value (атрибуту экземпляра Outer) — нет?
6
MarkdownРазбор концепции146 строк

Три уровня связи вложенных классов: от полного отсутствия до двусторонней

less_37__multiple_inheritance/theory_06__nestes_classes_relations.md

Продолжает тему вложенных классов тремя нарастающими примерами на Human/Heart. Пример 1: вложенность создаёт только пространство имён — ни some_human.heart, ни heart.Human() не существуют без явного кода (оба AttributeError). Пример 2: Human.__init__ явно создаёт self.heart = self.Heart(), давая одностороннюю связь человек→сердце, но обратной связи сердце→человек всё ещё нет. Пример 3: Heart.__init__ принимает owner: Human и сохраняет self.owner, создавая полноценную двустороннюю связь — сердце теперь знает, чьё оно, через self.owner.name.

  • Пример 1: Human()/Human.Heart() — раздельные объекты, обе попытки связать их — AttributeError
  • Пример 2: self.heart = self.Heart() в __init__ — alice.heart.beat() работает
  • Обратная связь всё ещё отсутствует: heart.name — AttributeError
  • Пример 3: Heart.__init__(self, owner: Human) — двусторонняя связь через self.owner
  • alice.heart.whose_heart() → 'Это сердце Alice'; итоговая таблица трёх примеров

Что выводит: Пример 1: два AttributeError — 'Human' object has no attribute 'heart' и 'Heart' object has no attribute 'Human'. Пример 2: 'Тук-тук' от alice.heart.beat(), затем AttributeError: 'Heart' object has no attribute 'name'. Пример 3: 'Тук-тук', 'Это сердце Alice', 'Alice', затем для bob — 'Это сердце Bob'.

Показать начало файла (146 строк всего)
## Вложенные классы: ключевая идея

Вложенные классы **не создают автоматически связи** 
* ни между классами, 
* ни между экземплярами.  

Они ТОЛЬКО создают пространство имён: `i = Outer.Inner()`

Все связи между объектами могут быть созданы ТОЛЬКО ЯВНО, так как:
* `Outer` не получает объект `Inner` по умолчанию;
* `Inner` не получает доступ к экземплярам `Outer` по умолчанию.

Предел "автоматического возможного":
* Доступ к атрибуту самого класса `Outer` (не не его экземпляров!) из методов класса `Inner`.

```python
class Outer:
    class_attr = 100   # атрибут КЛАССА

    class Inner:
        def show_outer_class_attr(self):
            print(Outer.class_attr)   # ✔ доступ есть

i = Outer.Inner()
i.show_outer_class_attr()
```

---

### Пример 1. Нет связи ни между классами, ни между объектами*

```python
class Human:
    class Heart:
        def beat(self):
            return "Тук-тук"

# Создаём отдельные объекты
some_human = Human()
heart = Human.Heart()

# Попытка узнать "чьё это сердце" со стороны человека (some_human):
try:
    print(some_human.heart.beat())
except Exception as e:
    print(f"{e.__class__.__name__}: {e}")  
    # AttributeError: 'Human' object has no attribute 'heart'

# Попытка узнать "что это за человек" со стороны сердца (heart):
try:
    print(heart.Human())
except Exception as e:
    print(f"{e.__class__.__name__}: {e}")
    # AttributeError: 'Heart' object has no attribute 'Human
```

**Вывод:**

* `Heart` существует как класс внутри `Human`.
* **Нет связи между объектом `alice` и объектом `heart`**, если мы её явно не создаём.
…
Проверьте себя: Что именно в коде примера 3 обеспечивает двустороннюю связь — какая конкретная строка позволяет сердцу узнать имя своего владельца?