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

Цели урока
  • Объяснить, когда наследование экономит дублирующийся код, и проверять это тестом «является разновидностью»
  • Определять родительский (Parent) и дочерний (Child) класс и понимать, как Python ищет метод — сначала в дочернем, затем в родительском
  • Переопределять метод в дочернем классе, чтобы изменить поведение
  • Использовать super() — и в __init__, и чтобы расширить метод родителя

В этом уроке

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

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

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

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

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

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

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

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

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.
Аналогия: шаблон производства машин — спортивная версия

У нас есть шаблон производства «машины» (родитель): 4 колеса, двигатель, руль, тормоза. Теперь нам нужна «спортивная машина». Мы не проектируем машину с нуля. Мы берём существующий шаблон и добавляем спойлер, а двигатель меняем. Каждая спортивная машина является машиной (она едет, тормозит), но не каждая машина спортивная.

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

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

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

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

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

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

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

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

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

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

Синтаксис

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:

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))

Вывод:

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:

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())

Вывод:

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__ — имя класса в виде текста.
         Animal
        /      \
      Dog      Cat

Итоги

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

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

Глава 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() — «принеси мне то, что возвращает родитель», а затем дополни.
Аналогия: семейный рецепт

Бабушка (родитель) печёт шоколадный торт по рецепту. Вы (потомок) наследуете рецепт. Переопределение = вы решаете испечь совсем другой торт с тем же названием «шоколадный торт», но веганский, с нуля. Расширение с super() = «сделай точно как бабушка» (super().bake()), а затем добавь глазурь, которую придумали вы. Вы не копируете весь бабушкин рецепт. Вы просто говорите «как у бабушки, плюс…».

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

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

Синтаксис

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:

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)

Вывод:

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:

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))

Вывод:

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(), автоматически дойдёт до переопределённой версии потомка.
  • Переопределённый метод сохраняет ту же сигнатуру (те же параметры).

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

Практика

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

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

К концу страницы вы будете уметь:

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

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

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

Что написать:

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

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

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

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

Dana: 12000
Avi: 20000
Avi manages 5 people
Показать решение
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))
Dana: 12000
Avi: 20000
Avi manages 5 people

Manager наследует __init__ и describe — не переписывая их — и добавляет только новое.

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

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

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

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

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

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

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

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

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

Что написать:

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

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

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

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

Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Показать решение
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())
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree

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

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

Что написать:

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

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

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)

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

Volvo 6 600
error: overloaded: max 1000 kg
Показать решение
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)
Volvo 6 600
error: overloaded: max 1000 kg

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

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

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

Что написать:

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

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

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

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

12000
23000
Показать решение
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())
12000
23000

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

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

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

Чему вы научитесь в проекте:

  • Строить родителя со всем общим и потомков, которые добавляют только отличающееся
  • Вызывать 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 не пишите: они приходят по наследству.

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

months must be 1-12

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

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)

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

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, и напечатайте его только для менеджеров из списка.

Показать решение
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)
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 — он наследует его как есть.

Бонус:

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

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

for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
Avi manages 5 people