Перейти к содержимому

Практика 2. Практическая работа 2. Наследование, полиморфизм и абстракция

Цель

Научиться проектировать иерархии классов на Python: строить базовые и производные классы, корректно расширять поведение через super(), переопределять методы, обеспечивать единый полиморфный интерфейс, перегружать операторы магическими методами и описывать контракты через абстрактные базовые классы (abc). По итогам работы вы должны уметь заменять ветвление по типам полиморфизмом, а «недоделанные» классы — запрещать к созданию средствами языка.


Краткая теория

Наследование позволяет создать производный класс на основе базового, переиспользуя и расширяя его код. Отношение между ними должно читаться как «является» (is-a): Dog является Animal. Для обращения к реализации родителя используют функцию super() — чаще всего при расширении __init__, чтобы не копировать инициализацию.

Переопределение (override) — это определение в подклассе метода с тем же именем, что и в базовом. Если базовый метод вызывает self.some(), то сработает реализация того класса, экземпляром которого является объект.

Полиморфизм — «один интерфейс, много реализаций». Код, написанный против общего интерфейса, не меняется при добавлении новых подклассов. В Python полиморфизм опирается на динамическую типизацию: важно наличие нужных методов, а не формальный тип объекта (утиная типизация).

Перегрузка операторов реализуется через магические (dunder) методы: __add__ для +, __eq__ для ==, __lt__ для <, __len__ для len(), __repr__ / __str__ для представления. Если операция не имеет смысла для данных типов, корректно вернуть NotImplemented.

Абстрактный базовый класс (модуль abc) описывает контракт: методы, помеченные @abstractmethod, обязан реализовать каждый потомок. Класс с непустым __abstractmethods__ нельзя инстанцировать — проверку выполняет метакласс ABCMeta в момент создания объекта. Декоратор @abstractmethod комбинируется с @property, @classmethod, @staticmethod и должен быть самым внутренним (ближайшим к def).

Подробности — в лекциях 2 («Наследование и полиморфизм») и 3 («Абстракция и абстрактные классы»).

Рекомендации по выполнению

  • Каждый класс снабжайте аннотациями типов и методом __repr__.
  • Не дублируйте логику родителя — расширяйте её через super().
  • Избегайте проверок type(x) == ...; полагайтесь на полиморфизм, а где без проверки не обойтись — используйте isinstance.
  • По возможности сопровождайте решения автотестами (pytest).

Задания

Задание 1. Иерархия животных (наследование и super())

Постройте иерархию с базовым классом Animal.

Требования:

  • Animal.__init__(self, name: str) сохраняет имя; метод speak() -> str возвращает "...", метод describe() -> str возвращает "{name} говорит: {speak()}".
  • Подклассы Dog, Cat переопределяют speak() ("Гав", "Мяу").
  • Dog.__init__ принимает дополнительный аргумент breed: str и вызывает super().__init__(name) — копировать присваивание self.name запрещено.
  • Класс LoudDog(Dog) переопределяет speak(), оборачивая результат родителя через super().speak() (например, "ГАВ!!!").
class Animal:
def __init__(self, name: str): ...
def speak(self) -> str: ...
def describe(self) -> str: ...

Проверьте: LoudDog("Рекс", "овчарка").describe() использует громкий speak(), хотя сам describe() не переопределялся.


Задание 2. Единый интерфейс и полиморфизм

Используя иерархию из задания 1, напишите функцию обработки списка.

Требования:

  • Функция make_them_talk(animals: list[Animal]) -> list[str] возвращает результат speak() каждого животного; код функции не должен меняться при добавлении нового вида.
  • Добавьте новый подкласс Cow (метод speak()"Му") и убедитесь, что функция работает без изменений.
  • Реализуйте «утиный» класс Robot с методом speak() -> str, но без наследования от Animal. Покажите, что make_them_talk принимает и его (обсудите в комментарии, почему это работает).

Задание 3. Перегрузка операторов: класс Money

Реализуйте класс Money для хранения суммы в одной валюте.

Требования:

  • Money(amount: float, currency: str = "RUB").
  • Магические методы:
    • __add__, __sub__ — складывают/вычитают суммы только одной валюты; при разных валютах вернуть NotImplemented или возбудить ValueError.
    • __mul__ — умножение суммы на число (Money * 3).
    • __eq__, __lt__ — сравнение сумм одной валюты (учтите разные валюты).
    • __repr__ — например Money(100.0, 'RUB').
  • Сумма не может быть отрицательной: проверяйте в __init__ (ValueError).
class Money:
def __init__(self, amount: float, currency: str = "RUB"): ...
def __add__(self, other: "Money") -> "Money": ...
def __eq__(self, other: object) -> bool: ...

Пример: Money(100) + Money(50) == Money(150), sorted([Money(30), Money(10), Money(20)]) упорядочивает по сумме.


Задание 4. Абстрактные фигуры (abc и полиморфизм)

Опишите контракт геометрической фигуры и его реализации.

Требования:

  • Абстрактный класс Shape(ABC) с абстрактными методами area() -> float и perimeter() -> float.
  • Реализации Circle(r), Rectangle(w, h), Square(side) (подумайте: Square наследует Rectangle или Shape?).
  • Проверьте, что Shape() нельзя создать (TypeError), а потомок без одного из методов остаётся абстрактным.
  • Функция total_area(shapes: list[Shape]) -> float суммирует площади любого набора фигур полиморфно.
  • Добавьте абстрактное свойство name (комбинация @property + @abstractmethod), возвращающее название фигуры.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...

Задание 5 (повышенной сложности). Платёжные системы

Спроектируйте мини-систему оплаты с абстрактным контрактом.

Требования:

  • Абстрактный класс PaymentMethod(ABC) с абстрактным методом pay(amount: Money) -> str (возвращает описание операции).
  • Реализации: CardPayment(card_no: str), CashPayment(), CryptoPayment(wallet: str). Каждая по-своему формирует результат pay.
  • Класс Checkout принимает любой PaymentMethod и метод process(method: PaymentMethod, amount: Money) -> str — полиморфная работа без isinstance-ветвлений.
  • (По желанию) Используйте паттерн «Шаблонный метод»: в PaymentMethod сделайте конкретный метод run(amount), который вызывает абстрактный шаг pay() и общий шаг логирования (hook-метод log() с реализацией по умолчанию).

Покажите работу Checkout с разными способами оплаты и суммами Money из задания 3.


Критерии оценки

КритерийДоля
Корректность иерархий, наследования и super() (задания 1–2)25%
Полиморфизм: единый интерфейс, работа с новыми подклассами и утиными типами20%
Перегрузка операторов: корректные магические методы, обработка несовместимых типов (задание 3)20%
Абстракция: ABC, @abstractmethod, запрет инстанцирования, абстрактное свойство (задания 4–5)20%
Качество кода: аннотации типов, __repr__, именование, отсутствие дублирования10%
Автотесты / повышенная сложность (задание 5)5%

Максимум — 100%. Снижение оценки за: копирование логики родителя вместо super(), ветвление по type(x) там, где уместен полиморфизм, отсутствие обработки несовместимых валют/типов в операторах.


Вопросы для самопроверки

  1. Что произойдёт, если в __init__ подкласса забыть вызвать super().__init__()? Как это проявится при обращении к атрибутам?
  2. Чем переопределение метода отличается от его расширения через super()? Приведите пример из задания 1.
  3. Почему функция make_them_talk не меняется при добавлении нового вида животного? Как это связано с переопределением методов?
  4. Может ли объект быть полиморфным без общего базового класса? Как это демонстрирует класс Robot?
  5. Какие магические методы нужно определить, чтобы экземпляры Money можно было складывать (+) и сортировать (sorted)?
  6. Зачем оператор возвращает NotImplemented, а не возбуждает ошибку сразу? Что Python делает дальше?
  7. Почему нельзя создать объект Shape()? Какой атрибут и какой метакласс за это отвечают?
  8. В каком порядке располагают @property и @abstractmethod и почему?
  9. Должен ли Square наследовать Rectangle? Какие плюсы и риски у этого решения (вспомните принцип подстановки Лисков)?
  10. Как паттерн «Шаблонный метод» применён в задании 5? Какой метод конкретный, а какие — абстрактные?