🐍 Самоучитель по Python для начинающих. Часть 19: Основы ООП – абстракция и полиморфизм
Расскажем, для чего полиморфизм и абстракция используются в программировании, и как они взаимодействуют с остальными фундаментальными концепциями ООП. В конце статьи – решение 10 задач, связанных с абстрактными и полиморфными классами.
Напомним, что эта статья – продолжение темы ООП в Python: предыдущая часть была посвящена инкапсуляции и наследованию.
Абстракция
Одна из основных целей использования абстракции в ООП – повышение
гибкости и упрощение разработки. Абстрактный подход помогает создавать
интерфейсы и классы, которые определяют только те свойства и методы, которые
необходимы для выполнения определенной задачи. Это позволяет создавать более
гибкие и масштабируемые приложения, которые легко поддаются изменению
и расширению.
Предположим, что нам нужно написать программу, которая работает
с графическими объектами разных типов. Для решения этой задачи удобно создать
абстрактный класс Shape (фигура), определяющий абстрактные методы,
которые могут быть использованы для работы с любой фигурой. Затем мы можем
создать конкретные классы для конкретных типов фигур – окружность, квадрат,
треугольник и т.д., которые расширяют базовый класс Shape. При этом мы можем
использовать только те свойства и методы, которые необходимы для выполнения
конкретной задачи, игнорируя детали реализации, которые не имеют значения в
данном контексте.
Абстрактный подход помогает эффективно решать ряд сложных задач:
Позволяет выделять существенные характеристики объекта, игнорируя все незначительные детали.
Принуждает подклассы к реализации конкретных методов или к выполнению определенных требований путем определения абстрактных методов или свойств. Таким образом, абстракция позволяет определять общие интерфейсы для классов, но при этом гарантирует, что каждый подкласс будет реализовывать свою версию этих методов или свойств.
Позволяет создавать общие модели объектов, которые могут использоваться для создания конкретных объектов.
Упрощает работу со сложными системами, которые включают множество взаимодействующих компонентов, и позволяет создавать расширяемые, модульные приложения.
Абстрактные классы в Python
Для работы с абстрактными классами в Python используют
модуль abc. Он предоставляет:
abc.ABC – базовый класс для создания абстрактных классов. Абстрактный класс содержит один или несколько абстрактных методов, то есть методов без определения (пустых, без кода). Эти методы необходимо переопределить в подклассах.
abc.abstractmethod – декоратор, который указывает, что метод является абстрактным. Этот декоратор применяется к методу внутри абстрактного класса. Класс, который наследует свойства и методы от абстрактного класса, должен реализовать все абстрактные методы, иначе он также будет считаться абстрактным.
Рассмотрим
пример абстрактного класса Book:
Класс Book имеет абстрактный метод
get_summary(). Два подкласса Book (Fiction и NonFiction) реализуют метод get_summary(), а третий подкласс Poetry – нет. Когда
мы создаем экземпляры Fiction и NonFiction и вызываем их методы get_summary(),
получаем ожидаемый результат:
Вывод:
А вот вызов Poetryприведет к ошибке, поскольку в этом подклассе метод get_summary() не реализован:
Вывод:
Приведенный выше пример показывает, что семейство
родственных классов (Fiction и NonFiction в нашем случае) может иметь общий
интерфейс (метод get_summary()), но реализация этого интерфейса может быть
разной. Мы также убедились, что любой подкласс Book должен реализовать метод
get_summary(), чтобы обеспечить согласованную, безошибочную работу приложения.
Теперь рассмотрим чуть более сложный пример, который
продемонстрирует, как можно комбинироватьабстракцию с другими концепциями ООП. Определим абстрактный класс Recipe
(рецепт), который имеет абстрактный метод cook(). Затем создадим три подкласса Entree,
Dessert и Appetizer (основное блюдо, десерт и закуска). Entree и Dessert имеют
свои собственные методы cook(), в отличие от Appetizer и PartyMix. PartyMix (орешки, чипсы, крекеры) является подклассом Appetizer и имеет свою реализацию
cook():
В этом примере наряду с абстракцией используются концепции полиморфизма
и наследования.
Наследование заключается в том, что подклассы Entree,
Dessert и PartyMix наследуют абстрактный метод cook() от абстрактного базового
класса Recipe. Это означает, что все они имеют ту же сигнатуру (название и
параметры) метода cook(), что и
абстрактный метод, определенный в классе Recipe.
Полиморфизм проявляется в том, что каждый подкласс класса
Recipe реализует метод cook() по-разному. Например, Entree реализует cook() для
вывода инструкций по приготовлению основного блюда на медленном огне, а Dessert
реализует cook() для вывода инструкций по смешиванию ингредиентов десерта. Эта
разница в реализации является примером полиморфизма, когда различные объекты
могут рассматриваться как объекты, которые относятся к одному типу, но при этом ведут себя по-разному:
Результат:
Вызов метода cook()
для подкласса Appetizer приведет к ожидаемой ошибке:
Полиморфизм позволяет обращаться с объектами разных классов
так, как будто они являются объектами одного класса. Реализовать полиморфизм можно через наследование, интерфейсы и перегрузку методов. Этот подход имеет несколько
весомых преимуществ:
Позволяет использовать различные реализации методов в зависимости от типа объекта, что делает код более универсальным и удобным для использования.
Уменьшает дублирование кода – можно написать одну функцию для работы с несколькими типами объектов.
Позволяет использовать общие интерфейсы и абстракции для работы с объектами разных типов.
Обеспечивает гибкость и расширяемость – можно добавлять новые типы объектов без необходимости изменять существующий код. Это дает возможность разработчикам встраивать новые функции в программу, не нарушая ее существующую функциональность.
Полиморфизм тесно связан с абстракцией:
Абстракция позволяет скрыть детали реализации объекта и предоставить только необходимый интерфейс для работы с ним. Это помогает упростить код, сделать его более понятным и гибким.
Полиморфизм предоставляет возможность использовать один и тот же интерфейс для работы с разными объектами, которые могут иметь различную реализацию. Этот подход значительно упрощает расширение функциональности ПО.
Таким образом, абстракция позволяет определить общий интерфейс для работы с объектами, а полиморфизм позволяет использовать этот интерфейс для работы с различными объектами, которые могут иметь различную реализацию.
Рассмотрим полиморфизм на примере класса Confectionary
(кондитерские изделия):
В этом примере мы определяем базовый класс Confectionary,
который имеет атрибуты name и price, а также метод describe(). Затем мы
определяем три подкласса класса Confectionary: Cake, Candy и Cookie. Cake и
Candy переопределяют метод describe() своими собственными реализациями, которые
включают тип кондитерского изделия (торт и конфеты соответственно), а Cookie
наследует дефолтный метод describe() от Confectionary.
Если создать экземпляры этих классов и вызвать их методы
describe(), можно убедиться, что результат зависит от реализации метода в
каждом конкретном подклассе:
Вывод:
Теперь разберем на примере класса Beverage (напиток)
взаимодействие полиморфизма с другими концепциями ООП. Beverage – родительский
класс, который содержит:
атрибуты названия, объема и цены;
методы для получения и установки этих атрибутов;
метод для вывода описания напитка.
Soda (газировка) – дочерний класс Beverage, у него есть
дополнительный атрибут flavor (вкус) и собственный метод describe(), включающий flavor.
DietSoda – еще один дочерний класс Soda, который наследует все атрибуты и
методы Soda, но переопределяет метод describe(), чтобы указать, что газировка
является диетической:
Этот пример демонстрирует:
Инкапсуляцию, поскольку атрибуты защищены символами подчеркивания и могут быть доступны только через методы getter и setter.
Наследование, поскольку Soda и DietSoda наследуют атрибуты и метод от Beverage.
Полиморфизм, поскольку каждый класс имеет свою собственную версию метода describe(), который возвращает различные результаты в зависимости от конкретного класса.
Вывод:
Практика
Задание 1
В далекой-далекой галактике Федерация ведет ожесточенную
войну с клингонами. Звездолеты Федерации оснащены мощными фазерами, а
клингонские корабли – смертоносными фотонными торпедами. Обе стороны
разработали усовершенствованные варп-двигатели для перемещения со сверхсветовой
скоростью, и оборудовали свои корабли системами самоуничтожения на случай чрезвычайной
ситуации.
Для игры, посвященной этой войне, нужно создать абстрактный класс Starship с методами warp_speed(),
fire_weapon() и self_destruct(). Кроме того, нужно создать два подкласса
FederationStarship и KlingonWarship, которые наследуют абстрактные методы
Starship и реализуют свои собственные версии методов warp_speed(), fire_weapon() и self_destruct().
Пример использования:
Вывод:
Решение:
Задание 2
Для ПО ресторана нужно разработать модуль, помогающий
контролировать использование фруктов и овощей на кухне. Создайте абстрактный
класс Ingredient с методами get_name() и get_quantity(). Затем создайте два
подкласса Vegetable и Fruit, которые наследуют абстрактные методы от Ingredient
и реализуют свои собственные версии методов get_name() и get_quantity().
Пример использования:
Вывод:
Решение:
Задание 3
Для военной стратегии необходимо создать абстрактный класс Soldier. Каждый солдат должен уметь двигаться, защищаться и атаковать, поэтому Soldier имеет три абстрактных
метода: move(), attack() и defend(). Два конкретных класса, Infantry (пехота) и
Cavalry (кавалерия), будут наследовать и реализовывать эти методы. В игре также
должен быть класс Army, который будет добавлять солдат в армию и выполнять
операции атаки и защиты.
Чтобы гарантировать, что используются только экземпляры
класса Soldier, нужно создать декоратор validatesoldier, который будет
проверять тип объекта. Если объект не является экземпляром класса Soldier,
декоратор выдаст ошибку TypeError. Декоратор будет применяться к методам
move(), attack() и defend() классов Infantry и Cavalry.
Примериспользования:
Вывод:
Решение:
Задание 4
Палеонтологам, работающим в заповеднике для динозавров, понадобилось
ПО для отслеживания множества травоядных и плотоядных подопечных. Данные,
которые нужно учитывать по каждому динозавру – имя, вид, рост, вес и рацион
питания.
Создайте абстрактный класс Dinosaur с методами get_personal_name(), get_breed(), get_height(),
get_weight() и
get_diet(). Затем создайте два
подкласса Carnivore (плотоядный) и Herbivore (травоядный), которые наследуют методы Dinosaur и реализуют свои собственные версии get_personal_name(), get_breed(), get_height(), get_weight() и get_diet(). Кроме того, создайте класс DinosaurPark, который содержит список динозавров и
имеет методы list_dinosaurs(), list_carnivores()
и list_herbivores() для вывода списков a) всех динозавров, b) плотоядных и c) травоядных особей.
Пример использования:
Вывод:
Решение:
Задание 5
Для учета музыкальных инструментов в оркестре нужно
создатьабстрактный класс Instrument с
методами get_name(), get_type(), get_sound() и play(). Два подкласса
StringedInstrument (струнные) и PercussionInstrument (ударные) наследуют методы
Instrument и реализуют свои собственные версии методов get_name(), get_type(),
get_sound() и play(). Кроме того, необходимо реализовать класс Orchestra: он добавляет новые
инструменты и имеет методы list_instruments(), list_stringed_instruments(),
list_percussion_instruments(), которые выводят списки a) всех инструментов, b) ударных, c) струнных.
Пример использования:
Вывод:
Решение:
Задание 6
Напишите класс FilmCatalogue (каталог фильмов), который отвечает
за ведение фильмотеки. FilmCatalogue должен поддерживать различные типы
кинокартин, чтобы пользователи могли искать фильмы по определенному жанру. Для
этого необходимо создать новые классы для различных жанров (например, Horror,
Action, Romance), которые наследуют класс Movie и переопределяют метод play()
для вывода информации о том, к какому жанру относится фильм.
Пример использования:
Вывод:
Решение:
Задание 7
Для CRM винодельни нужно написать модуль, отвечающий за учет
красных, белых и розовых вин, каждое из которых имеет свое название, сорт винограда,
год и температуру подачи. Создайте базовый класс Wine с атрибутами name, grape
и year. Затем создайте три подкласса RedWine, WhiteWine и RoseWine, которые
наследуют методы и атрибуты от Wine и реализуют свои собственные версии метода
serve(). Кроме того, создайте класс Winery, который ведет список вин и имеет
метод serve_wines(), вызывающий метод serve() для каждого вина.
Пример использования:
Вывод:
Решение:
Задание 8
Для ПО аэропорта нужно разработать модуль, отслеживающий
пассажирские и грузовые самолеты, которые отличаются моделью, производителем,
вместимостью и грузоподъемностью. Создайте базовый класс Aircraft (воздушное
судно) с атрибутами model, manufacturer и capacity. Затем создайте два
подкласса PassengerAircraft и CargoAircraft, которые наследуют атрибуты и
методы от Aircraft и реализуют свои собственные версии метода fly(). В
дополнение создайте класс Airport, который содержит список самолетов и имеет
метод takeoff(), вызывающий метод fly() для каждого самолета.
Пример использования:
Вывод:
Решение:
Задание 9
Необходимо реализовать модуль, отвечающий за обработку
данных о тестировании конфигурации настольных компьютеров и ноутбуков, каждый
из которых отличается моделью, процессором, памятью и производительностью. Создайте
базовый класс Computer с атрибутами model, processor и memory. Затем создайте
два подкласса Desktop и Laptop, которые наследуют атрибуты и методы Computer и
реализуют свои собственные версии метода run(). В дополнение, создайте класс
ComputerStore, который содержит список компьютеров и имеет метод run_tests(),
вызывающий метод run() для каждого компьютера. Используйте декораторы для
вывода результатов.
Примериспользования:
Вывод:
Решение:
Задание 10
Определите базовый класс Cryptocurrency, имеющий атрибуты:
name – название;
symbol – символ-тикер;
minable – возможность добычи майнингом;
rate_to_usd – текущий курс к доллару;
anonymous – наличие анонимных транзакций.
Затем создайте три подкласса Nano, Iota и Stellar, которые наследуют атрибуты и
методы родительского класса Cryptocurrency, и обладают дополнительными свойствами, влияющими
на размер вознаграждения за майнинг:
атрибут block_lattice у Nano;
tangle у Iota.
Кроме того, нужно реализовать:
Декоратор minable_required, который проверяет, можно ли майнить криптовалюту перед вызовом метода mining_reward(), и выводит сообщение, если ее майнить нельзя.
Функцию print_info, которая принимает на вход экземпляр криптовалюты и выводит информацию о монете, включая название, символ, возможность добычи, курс к доллару США, анонимность и наличие блок-решетки.
Пример использования:
Вывод:
Решение:
Подведем итоги
Объектно-ориентированный подход обладает несколькими важными
преимуществами, среди которых:
Модульность – ООП позволяет разбивать сложные задачи на более мелкие и простые составляющие.
Возможность переиспользования –можно многократно использовать однажды написанный код с помощью наследования и полиморфизма.
Инкапсуляция – можно скрыть детали реализации одних модулей от других частей программы. Это помогает сделать код более безопасным, поскольку другие части вашей программы не могут получить доступ или изменить данные или методы, которые не предназначены для использования извне.
Гибкость – ООП позволяет легко создавать новые объекты и изменять существующие. Можно добавлять или удалять методы и свойства по мере необходимости, а также создавать новые объекты, которые наследуют атрибуты и методы существующих классов.
Простота сопровождения и отладки. Разделение код на составляющие модули упрощает структуру программы и помогает изолировать ошибки. Кроме того, использование описательных имен классов и методов делает код более читабельным и понятным.
В следующей части будем изучать основы разработки приложений с
графическим интерфейсом.
Комментарии