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