# Наследование и super: похожие классы без дублирования кода<no value>

## В этом уроке

Урок построен как одна история, в том же порядке, что и в презентации:

1. **Глава 1** — зачем наследование: один и тот же код дважды и как наследовать вместо копирования
2. **Глава 2** — переопределение и `super()`: наследование, но не совсем — меняем только часть поведения

В конце урока — практика и **мини-проект**, который связывает обе главы.

## Глава 1: Зачем наследование — родитель, потомок и отношение «является разновидностью»

### Что мы изучим?

- **Зачем** нужно наследование: проблема почти одинаковых классов.
- **Родительский класс** (Parent Class, Base Class) и **дочерний класс** (Child Class, Subclass).
- **Синтаксис** наследования в Python.
- **Что передаётся** при наследовании.
- **Тест is-a**: когда наследование подходит, а когда нет.

### Зачем это нужно?

В уроке о классах и объектах мы спрашивали: что будет, если добавить «сберегательный счёт», похожий на обычный, но с процентами? В процедурной версии каждая функция наполнялась `if account["type"] == ...`. Посмотрим на ту же проблему в ООП, **без** наследования:

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

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager:
    def __init__(self, name, salary, team_size):
        self.name = name
        self.salary = salary
        self.team_size = team_size

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

    def team_report(self):
        return self.name + " manages " + str(self.team_size) + " people"
```

`Manager` **копирует** почти весь `Employee`: `name`, `salary`, `monthly_cost`, `describe`. Что будет, когда стоимость работодателя изменится с 1.25 на 1.3? Придётся исправлять **в двух местах**, а если есть ещё `Intern`, `Contractor` и `Director` — в пяти. **Это нарушение DRY на уровне классов.**

И ещё одно: **менеджер — это сотрудник.** Всё, что верно для сотрудника, верно и для менеджера, а у менеджера к тому же есть дополнительное. Код должен отражать это отношение.

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

#### Наследование

**Наследование** (Inheritance) = механизм, при котором новый класс **автоматически получает** все атрибуты и методы существующего класса и может **добавлять** или **менять** части.

- **Родительский класс** (Parent, Base, Superclass) — общий класс. `Employee`.
- **Дочерний класс** (Child, Derived, Subclass) — частный класс, который **наследует**. `Manager`.

{{< box "analogy" "Аналогия: шаблон производства машин — спортивная версия" >}}
У нас есть шаблон производства «машины» (родитель): 4 колеса, двигатель, руль, тормоза. Теперь нам нужна «спортивная машина». **Мы не проектируем машину с нуля.** Мы берём существующий шаблон и **добавляем** спойлер, а двигатель **меняем**. Каждая спортивная машина **является машиной** (она едет, тормозит), но не каждая машина спортивная.
{{< /box >}}

#### Отношение «является разновидностью» (is-a)

Наследование подходит, **только** когда можно честно сказать: **«потомок — разновидность родителя»**.

| Предложение | Наследование? |
|---|---|
| Менеджер — **разновидность** сотрудника | ✓ `Manager(Employee)` |
| Собака — **разновидность** животного | ✓ `Dog(Animal)` |
| Сберегательный счёт — **разновидность** банковского счёта | ✓ `SavingsAccount(BankAccount)` |
| Машина — **разновидность** двигателя? | ✗ У машины **есть** двигатель (has-a), а это **композиция** (Composition), тема проектирования ООП |
| Заказ — **разновидность** клиента? | ✗ У заказа **есть** клиент |

#### Что передаётся при наследовании?

Потомок получает от родителя **всё**:

- `__init__` (если потомок не определяет собственный `__init__`).
- **Все методы**.
- **Атрибуты класса**.

А потомок может:

- **Добавлять** новые методы и атрибуты.
- **Менять** (переопределять, Override) существующие методы. Изучим это в следующей главе вместе с `super()`.

#### Поиск по слоям — снова

Когда вы пишете `manager.describe()`, Python ищет: **(1)** на объекте, **(2)** в его классе (`Manager`), **(3)** в родителе (`Employee`)... и останавливается на первом месте, где нашёл. Это та же идея поиска по слоям, что мы видели в LEGB и в порядке поиска атрибутов.

### Синтаксис

```python
class Parent:
    def __init__(self, x):
        self.x = x

    def shared_method(self):
        return ...

class Child(Parent):             # Child inherits from Parent
    def extra_method(self):      # new method, only in Child
        return ...

c = Child(5)                     # uses Parent.__init__ (Child has none of its own)
c.shared_method()                # inherited
c.extra_method()                 # Child's own
```

- Имя родителя пишется **в скобках** после имени потомка.
- Потомок без всяких дополнений: `class Child(Parent): pass` — допустимый класс, идентичный родителю.

### Первый пример кода

`week-3/employees.py`:

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager(Employee):
    def team_report(self, team_size):
        return self.name + " manages " + str(team_size) + " people"

dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)

print(dana.describe())
print(avi.describe())
print(avi.monthly_cost())
print(avi.team_report(5))
```

**Вывод:**

```text
Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 people
```

### Разбор кода построчно

| Код | Объяснение |
|---|---|
| `class Manager(Employee):` | `Manager` **наследует** `Employee`. |
| `Manager("Avi", 20000)` | У `Manager` нет собственного `__init__`, поэтому Python находит `__init__` у `Employee` и использует его. Ави получает `name` и `salary`. |
| `avi.describe()` | В `Manager` нет `describe`. Python поднимается к `Employee` и находит его. **`self` по-прежнему Ави.** |
| `avi.monthly_cost()` | То же самое, и `EMPLOYER_COST_RATE` тоже находится через родителя. |
| `avi.team_report(5)` | Метод, который есть **только** в `Manager`. |
| `dana.team_report(...)` | Выбросил бы `AttributeError`: обычный сотрудник **не** наследует менеджера. Наследование идёт только **сверху вниз**. |

**А исправление 1.25 → 1.3?** Меняете **одну строку** в `Employee`, и все менеджеры (а в будущем — любой вид сотрудников) обновятся.

### Ещё один пример

**Иерархия с несколькими потомками: Animal / Dog / Cat.** `week-3/animals.py`:

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

    def birthday(self):
        self.age = self.age + 1

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"

class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"

class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"

rex = Dog("Rex", 3)
mitzi = Cat("Mitzi", 5)

rex.birthday()
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())

for animal in [rex, mitzi]:
    print(type(animal).__name__, "->", animal.describe())
```

**Вывод:**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Dog -> Rex (4 years old)
Cat -> Mitzi (5 years old)
```

**Разбор:**

- **Два потомка одного родителя**: `Dog` и `Cat` разделяют всё общее (`name`, `age`, `birthday`, `describe`), и каждый добавляет своё уникальное.
- `rex.birthday()` — метод, **написанный один раз** в `Animal`, который меняет состояние **rex**.
- В цикле `animal.describe()` работает для **любого** вида `Animal`, без проверки, что это за вид. Это намёк на **полиморфизм**, в который мы углубимся в следующем уроке.
- `type(animal).__name__` — имя класса в виде текста.

```text
         Animal
        /      \
      Dog      Cat
```

### Итоги

- **Наследование** = дочерний класс получает все атрибуты и методы родительского класса и может добавлять и менять.
- Синтаксис: `class Child(Parent):`.
- Оно решает проблему **дублирования** между похожими классами (DRY) и отражает отношение «**является разновидностью**» (is-a).
- Python ищет метод в классе объекта, а если не нашёл — поднимается к родителю.
- Наследование однонаправленное: родитель не знает, что добавлено в потомке.
- **Не** наследуйте, когда отношение — «**имеет**» (has-a). Тогда используют композицию (тема проектирования ООП).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Официальная документация

- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)
- [isinstance()](https://docs.python.org/3/builtins/functions.html#isinstance)

<!-- docs:end -->

## Глава 2: Переопределение и `super()` — меняем только то, что отличается

### Что мы изучим?

- **`super()`**: как потомок использует код родителя.
- **`__init__` в потомке**: добавление атрибутов без копирования родительских.
- **Переопределение метода** (Method Overriding): потомок **заменяет** метод родителя.
- **Расширение** (Extending): потомок **дополняет** метод родителя, а не заменяет его полностью.

### Зачем это нужно?

Здесь простое наследование упирается в стену: `Truck`, который наследует `Vehicle`, нуждается в дополнительном атрибуте (`cargo_kg`), то есть в собственном `__init__`. Но если написать новый `__init__` в Truck, он **заменит** `__init__` Vehicle, и тогда `brand` и `wheels` не будут инициализированы — если только мы не **скопируем** код. А именно это наследование и должно было предотвратить.

И вторая проблема: иногда потомок должен вести себя **по-другому** в том же действии. Менеджер получает бонус в расчёте стоимости. Сберегательный счёт не позволяет снять ниже минимума. Нужен способ **изменить** метод, пришедший по наследству.

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

#### Переопределение метода

Если в потомке определить метод **с тем же именем**, что и в родителе, **для объектов типа Child побеждает версия потомка**. Это называется **переопределением** (Override). Почему? Вспомните порядок поиска: Python ищет **сначала** в классе объекта и только если не нашёл, поднимается к родителю. Если метод есть в потомке, Python даже не доходит до версии родителя.

#### `super()` — «версия родителя»

**`super()`** возвращает специальный объект, через который вызывают **метод родителя** на **том же `self`**. Два главных применения:

1. **В `__init__`**: `super().__init__(...)` — «сначала инициализируй всё, что инициализирует родитель», а затем добавь то, что **уникально** для потомка.
2. **При расширении метода**: `super().describe()` — «принеси мне то, что возвращает родитель», а затем дополни.

{{< box "analogy" "Аналогия: семейный рецепт" >}}
Бабушка (родитель) печёт шоколадный торт по рецепту. Вы (потомок) **наследуете** рецепт. **Переопределение** = вы решаете испечь совсем другой торт с тем же названием «шоколадный торт», но веганский, с нуля. **Расширение с `super()`** = «сделай точно как бабушка» (`super().bake()`), **а затем** добавь глазурь, которую придумали вы. Вы не копируете весь бабушкин рецепт. Вы просто говорите «как у бабушки, плюс...».
{{< /box >}}

#### Три варианта для метода в потомке

| Вариант | Что вы делаете | Когда |
|---|---|---|
| **Унаследовать** | Ничего не пишете | Поведение родителя подходит |
| **Переопределить** (Override) | Пишете метод с тем же именем, без `super()` | Поведение совсем другое |
| **Расширить** (Extend) | Пишете метод с тем же именем, а внутри — `super().method()` | «Как у родителя, плюс что-то» |

### Синтаксис

```python
class Parent:
    def __init__(self, a):
        self.a = a

    def describe(self):
        return "a=" + str(self.a)

    def greet(self):
        return "Hello from Parent"

class Child(Parent):
    def __init__(self, a, b):
        super().__init__(a)          # let Parent set up self.a
        self.b = b                   # then add what is new in Child

    def describe(self):              # EXTEND: reuse Parent's result
        return super().describe() + ", b=" + str(self.b)

    def greet(self):                 # OVERRIDE: replace completely
        return "Hello from Child"
```

**Обратите внимание:** в `super().__init__(a)` `self` **не** передают. `super()` уже «знает», о каком объекте речь.

### Первый пример кода

**Решаем проблему Truck.** `week-3/vehicles_super.py`:

```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels

    def describe(self):
        return self.brand + " with " + str(self.wheels) + " wheels"

class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.cargo_kg = 0

    def load(self, kg):
        if self.cargo_kg + kg > self.max_load_kg:
            raise ValueError("over the limit: max " + str(self.max_load_kg) + " kg")
        self.cargo_kg = self.cargo_kg + kg

    def describe(self):
        return super().describe() + ", carrying " + str(self.cargo_kg) + "/" + str(self.max_load_kg) + " kg"

volvo = Truck("Volvo", 1000)
volvo.load(600)
print(volvo.describe())

try:
    volvo.load(500)
except ValueError as error:
    print("Rejected:", error)

print(volvo.brand, volvo.wheels)
```

**Вывод:**

```text
Volvo with 6 wheels, carrying 600/1000 kg
Rejected: over the limit: max 1000 kg
Volvo 6
```

### Разбор кода построчно

| Код | Объяснение |
|---|---|
| `def __init__(self, brand, max_load_kg):` | У Truck **собственный** `__init__` с параметрами, подходящими для грузовика. |
| `super().__init__(brand, 6)` | «Выполни `__init__` Vehicle **на этом объекте**». Это создаёт `self.brand` и `self.wheels`. У грузовика всегда 6 колёс, поэтому значение решает потомок. |
| `self.max_load_kg = ...`, `self.cargo_kg = 0` | **Только** то, что ново в Truck. Без копирования ни одной строки из Vehicle. |
| `def describe(self):` в Truck | **Расширение**: то же имя, что и в Vehicle. |
| `super().describe()` | Получает `"Volvo with 6 wheels"` от родителя и дополняет данными о грузе. |
| `volvo.brand`, `volvo.wheels` | Существуют, потому что их создал `super().__init__`. |

**Если завтра Vehicle изменится**, например добавит `self.year`, грузовики получат это автоматически через `super().__init__`. Если бы мы скопировали код, пришлось бы помнить, что нужно обновить и Truck.

### Ещё один пример

**Полное переопределение, расширение и `super()` внутри бизнес-правил.** `week-3/staff.py`:

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def title(self):
        return "Employee"

    def describe(self):
        return self.title() + " " + self.name + " - cost " + str(self.monthly_cost())

class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus

    def title(self):
        return "Manager"

class Intern(Employee):
    def monthly_cost(self):
        return self.salary

    def title(self):
        return "Intern"

staff = [Employee("Dana", 12000), Manager("Avi", 20000, 3000), Intern("Noa", 5000)]

for person in staff:
    print(person.describe())

print("Total monthly cost:", sum(person.monthly_cost() for person in staff))
```

**Вывод:**

```text
Employee Dana - cost 15000.0
Manager Avi - cost 28000.0
Intern Noa - cost 5000
Total monthly cost: 48000.0
```

**Разбор:**

- `Manager.monthly_cost` — **расширение**: «как у обычного сотрудника (`super().monthly_cost()` = 25 000), **плюс** бонус». Если коэффициент стоимости работодателя изменится, менеджеры получат изменение автоматически.
- `Intern.monthly_cost` — **полное переопределение**: стажёр не несёт расходов работодателя, поэтому совершенно другой расчёт, без `super()`.
- `title()` — каждый класс переопределяет его.
- **Интересный момент:** `describe()` написан **только один раз**, в `Employee`. Он вызывает `self.title()` и `self.monthly_cost()`. Когда объект — Manager, `self.monthly_cost()` доходит до **версии Manager**! Метод родителя автоматически использует переопределённые версии потомка. Это основа **полиморфизма** (следующий урок).
- `Intern` не определил `__init__`, поэтому использует `__init__` класса `Employee` (обычное наследование).

### Итоги

- **Переопределение** (Override) = метод в потомке с тем же именем, что и в родителе. Побеждает версия потомка.
- **`super()`** = доступ к версии родителя на том же `self`.
- Потомок с собственным `__init__` **обязан** вызвать `super().__init__(...)` (без `self`), а затем добавить только новое.
- **Расширение**: `super().method()` + дополнение. **Полное переопределение**: без `super()`.
- `self.method()` внутри переопределённого метода = рекурсия. Для версии родителя — `super().method()`.
- Метод родителя, который вызывает `self.x()`, автоматически дойдёт до переопределённой версии потомка.
- Переопределённый метод сохраняет **ту же сигнатуру** (те же параметры).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Официальная документация

- [super()](https://docs.python.org/3/builtins/functions.html#super)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

**Что вы отрабатываете на этой странице:** наследование — родитель и потомок, тест «является разновидностью», расширение `__init__` с помощью `super()` и переопределение метода (Override).

**К концу страницы вы будете уметь:**
- Писать потомка, который наследует родителя и добавляет только новое
- Решать, когда подходит наследование, с помощью теста «является разновидностью»
- Вызывать `super().__init__`, чтобы родитель инициализировал свою часть
- Переопределять метод и использовать внутри него версию родителя

### Упражнение 1: Сотрудник и менеджер

**Что вы строите:** сотрудника и менеджера — где менеджер является разновидностью сотрудника, поэтому общее не пишется заново.

**Что написать:**
1. Класс с именем `Employee` с именем и зарплатой.
2. В `Employee`: `describe`, который возвращает имя и зарплату, как в выводе.
3. Класс с именем `Manager`, который наследует `Employee`, — без собственного `__init__`.
4. Только в `Manager`: `manage`, который получает размер команды и возвращает предложение, как в выводе.

**Проверочный код:**

```python
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

**Желаемый вывод:**

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return self.name + ": " + str(self.salary)


class Manager(Employee):
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"


dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

`Manager` **наследует** `__init__` и `describe` — не переписывая их — и добавляет только новое.
{{< /solution >}}

### Упражнение 2: is-a — является разновидностью

**Что вы делаете:** решаете — без кода, — когда подходит наследование.

**Задание:** для каждой пары ответьте: должен ли первый наследовать второй? Объясните с помощью предложения «X — разновидность Y», а если предложение неверно — какое на самом деле между ними отношение.

1. `Dog` и `Animal`
2. `Car` и `Engine`
3. `SavingsAccount` и `BankAccount`
4. `Square` и `Color`

**Что сдать:** да или нет для каждой пары и одно предложение объяснения.

{{< solution >}}
1. **Да** — собака **является разновидностью** животного.
2. **Нет** — машина **не** разновидность двигателя. У машины **есть** двигатель (has-a). Это случай для композиции, а не для наследования.
3. **Да** — сберегательный счёт **является разновидностью** банковского счёта.
4. **Нет** — квадрат не разновидность цвета. Возможно, у квадрата **есть** цвет.

Наследование — только когда предложение «X — разновидность Y» верно.
{{< /solution >}}

### Упражнение 3: Дерево животных

**Что вы строите:** маленькое дерево наследования — животное и два вида животных, у каждого своё действие.

**Что написать:**
1. Класс с именем `Animal` с именем и возрастом и `describe`, который возвращает имя и возраст, как в выводе.
2. Класс с именем `Dog`, который наследует `Animal`, с `fetch`, который возвращает предложение из вывода.
3. Класс с именем `Cat`, который наследует `Animal`, с `climb`, который возвращает предложение из вывода.

**Проверочный код:**

```python
rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

**Желаемый вывод:**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```

{{< solution >}}
```python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"


class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"


class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"


rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```
{{< /solution >}}

### Упражнение 4: super().__init__

**Что вы строите:** грузовик — транспортное средство, у которого всегда 6 колёс, а ещё есть максимальный вес груза.

**Что написать:**
1. Класс с именем `Vehicle` с маркой и числом колёс.
2. Класс с именем `Truck`, который наследует `Vehicle`. Его `__init__` получает марку и максимальный вес.
3. Внутри `__init__` у `Truck`: вызовите `super().__init__` с маркой и 6 колёсами и инициализируйте текущий вес нулём.
4. `load` — получает килограммы и добавляет их к весу. Если максимум превышен: `ValueError` с сообщением из вывода.

**Проверочный код:**

```python
truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

**Желаемый вывод:**

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

{{< solution >}}
```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels


class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.load_kg = 0

    def load(self, kg):
        if self.load_kg + kg > self.max_load_kg:
            raise ValueError("overloaded: max " + str(self.max_load_kg) + " kg")
        self.load_kg = self.load_kg + kg


truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

`super().__init__(brand, 6)` инициализирует часть родителя. После этого потомок добавляет собственные атрибуты.
{{< /solution >}}

### Упражнение 5: Переопределение с super

**Что вы строите:** менеджера, у которого месячная стоимость — зарплата плюс бонус.

**Что написать:**
1. Класс с именем `Employee` с именем и зарплатой и `monthly_cost`, который возвращает зарплату.
2. Класс с именем `Manager`, который наследует `Employee` и получает ещё и бонус. В его `__init__` вызовите `super().__init__`.
3. В `Manager` переопределите `monthly_cost`: версия родителя через `super()` плюс бонус.

**Проверочный код:**

```python
print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

**Желаемый вывод:**

```text
12000
23000
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

```text
12000
23000
```

`super().monthly_cost()` вызывает версию родителя. Если расчёт базовой зарплаты изменится, `Manager` получит изменение автоматически.
{{< /solution >}}

## Мини-проект: Сотрудники компании

**Какую систему вы строите:** систему сотрудников для компании. В компании есть обычные сотрудники, менеджеры и стажёры. Все они сотрудники — у них есть имя и зарплата, — но каждый вид отображается по-своему, а у менеджера есть ещё и бонус. Система отображает каждого сотрудника и считает месячную стоимость всех вместе.

**Чему вы научитесь в проекте:**
- Строить родителя со всем общим и потомков, которые добавляют только отличающееся
- Вызывать `super().__init__` из потомка
- Переопределять метод и расширять метод с помощью `super()`
- Видеть, как метод, написанный один раз в родителе, правильно работает для каждого вида

**Что построить — шаг за шагом:**

**Шаг 1 — сотрудник (`Employee`).** `__init__` получает имя и зарплату. Три метода:
- `title` — возвращает слово Employee;
- `monthly_cost` — возвращает зарплату;
- `describe` — возвращает имя, роль и месячную стоимость, как в выводе. Он использует `title` и `monthly_cost`.

**Шаг 2 — менеджер (`Manager`).** Наследует `Employee` и получает ещё и бонус. В `__init__` вызовите `super().__init__` и сохраните бонус. Переопределите `title` — слово Manager. Расширьте `monthly_cost`: результат `super()` плюс бонус.

**Шаг 3 — стажёр (`Intern`).** Наследует `Employee` и получает ещё и число месяцев стажировки. В `__init__` проверьте, что число от 1 до 12, — иначе `ValueError`, — а затем вызовите `super().__init__`. Переопределите `title` — слово Intern. `monthly_cost` и `describe` **не** пишите: они приходят по наследству.

**Сообщение об ошибке — именно такое:**

```text
months must be 1-12
```

**Проверочный код:**

```python
staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

**Желаемый вывод:**

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

**К концу проекта вы будете уметь:**
- Проектировать иерархию: что поднимается в родителя, а что остаётся в потомке
- Различать три варианта для метода в потомке: унаследовать, переопределить или расширить с помощью `super()`
- Понимать, почему `describe` показывает каждому сотруднику **его собственные** роль и стоимость, хотя написан один раз

**Бонус:** добавьте в `Manager` метод `manage`, который получает размер команды и возвращает предложение вида Avi manages 5 people, и напечатайте его только для менеджеров из списка.

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def title(self):
        return "Employee"

    def monthly_cost(self):
        return self.salary

    def describe(self):
        return self.name + " (" + self.title() + "): " + str(self.monthly_cost())


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def title(self):
        return "Manager"

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


class Intern(Employee):
    def __init__(self, name, salary, months):
        if months < 1 or months > 12:
            raise ValueError("months must be 1-12")
        super().__init__(name, salary)
        self.months = months

    def title(self):
        return "Intern"


staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

- `describe` написан **один раз** в `Employee`, но каждый сотрудник получает **свои** `title()` и `monthly_cost()` — потому что Python ищет метод сначала в потомке.
- `Manager.monthly_cost` **расширяет**: `super().monthly_cost()` возвращает зарплату, и к ней добавляется бонус. `title` **полностью переопределяет**.
- `Intern` не пишет `monthly_cost` — он **наследует** его как есть.

**Бонус:**

```python
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"
```

(Метод внутри `Manager`.)

```python
for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
```

```text
Avi manages 5 people
```
{{< /solution >}}

<!-- exercises:end -->
