Напомним, что эта статья – продолжение темы ООП в Python: предыдущая часть была посвящена инкапсуляции и наследованию.
Абстракция
Одна из основных целей использования абстракции в ООП – повышение гибкости и упрощение разработки. Абстрактный подход помогает создавать интерфейсы и классы, которые определяют только те свойства и методы, которые необходимы для выполнения определенной задачи. Это позволяет создавать более гибкие и масштабируемые приложения, которые легко поддаются изменению и расширению.
Предположим, что нам нужно написать программу, которая работает с графическими объектами разных типов. Для решения этой задачи удобно создать абстрактный класс Shape (фигура), определяющий абстрактные методы, которые могут быть использованы для работы с любой фигурой. Затем мы можем создать конкретные классы для конкретных типов фигур – окружность, квадрат, треугольник и т.д., которые расширяют базовый класс Shape. При этом мы можем использовать только те свойства и методы, которые необходимы для выполнения конкретной задачи, игнорируя детали реализации, которые не имеют значения в данном контексте.
Абстрактный подход помогает эффективно решать ряд сложных задач:
- Позволяет выделять существенные характеристики объекта, игнорируя все незначительные детали.
- Принуждает подклассы к реализации конкретных методов или к выполнению определенных требований путем определения абстрактных методов или свойств. Таким образом, абстракция позволяет определять общие интерфейсы для классов, но при этом гарантирует, что каждый подкласс будет реализовывать свою версию этих методов или свойств.
- Позволяет создавать общие модели объектов, которые могут использоваться для создания конкретных объектов.
- Упрощает работу со сложными системами, которые включают множество взаимодействующих компонентов, и позволяет создавать расширяемые, модульные приложения.
Абстрактные классы в Python
Для работы с абстрактными классами в Python используют модуль abc. Он предоставляет:
- abc.ABC – базовый класс для создания абстрактных классов. Абстрактный класс содержит один или несколько абстрактных методов, то есть методов без определения (пустых, без кода). Эти методы необходимо переопределить в подклассах.
- abc.abstractmethod – декоратор, который указывает, что метод является абстрактным. Этот декоратор применяется к методу внутри абстрактного класса. Класс, который наследует свойства и методы от абстрактного класса, должен реализовать все абстрактные методы, иначе он также будет считаться абстрактным.
Рассмотрим пример абстрактного класса Book:
from abc import ABC, abstractmethod
class Book(ABC):
def __init__(self, title, author):
self.title = title
self.author = author
@abstractmethod
def get_summary(self):
pass
class Fiction(Book):
def get_summary(self):
print(f'"{self.title}" - роман в стиле исторический фикшн, автор - {self.author}')
class NonFiction(Book):
def get_summary(self):
print(f'"{self.title}" - книга в стиле нон фикшн, автор - {self.author}')
class Poetry(Book):
pass
Класс Book имеет абстрактный метод get_summary(). Два подкласса Book (Fiction и NonFiction) реализуют метод get_summary(), а третий подкласс Poetry – нет. Когда мы создаем экземпляры Fiction и NonFiction и вызываем их методы get_summary(), получаем ожидаемый результат:
fiction_book = Fiction("Террор", "Дэн Симмонс")
nonfiction_book = NonFiction("Как писать книги", "Стивен Кинг")
fiction_book.get_summary()
nonfiction_book.get_summary()
Вывод:
"Террор" - роман в стиле исторический фикшн, автор - Дэн Симмонс
"Как писать книги" - книга в стиле нон фикшн, автор - Стивен Кинг
А вот вызов Poetry приведет к ошибке, поскольку в этом подклассе метод get_summary() не реализован:
poetry_book = Poetry("Стихотворения", "Борис Пастернак")
Вывод:
TypeError: Can't instantiate abstract class Poetry with abstract methods get_summary
Приведенный выше пример показывает, что семейство родственных классов (Fiction и NonFiction в нашем случае) может иметь общий интерфейс (метод get_summary()), но реализация этого интерфейса может быть разной. Мы также убедились, что любой подкласс Book должен реализовать метод get_summary(), чтобы обеспечить согласованную, безошибочную работу приложения.
Теперь рассмотрим чуть более сложный пример, который продемонстрирует, как можно комбинировать абстракцию с другими концепциями ООП. Определим абстрактный класс Recipe (рецепт), который имеет абстрактный метод cook(). Затем создадим три подкласса Entree, Dessert и Appetizer (основное блюдо, десерт и закуска). Entree и Dessert имеют свои собственные методы cook(), в отличие от Appetizer и PartyMix. PartyMix (орешки, чипсы, крекеры) является подклассом Appetizer и имеет свою реализацию cook():
from abc import ABC, abstractmethod
class Recipe(ABC):
@abstractmethod
def cook(self):
pass
class Entree(Recipe):
def __init__(self, ingredients):
self.ingredients = ingredients
def cook(self):
print(f"Готовим на медленном огне смесь ингредиентов ({', '.join(self.ingredients)}) для основного блюда")
class Dessert(Recipe):
def __init__(self, ingredients):
self.ingredients = ingredients
def cook(self):
print(f"Смешиваем {', '.join(self.ingredients)} для десерта")
class Appetizer(Recipe):
pass
class PartyMix(Appetizer):
def cook(self):
print("Готовим снеки - выкладываем на поднос орешки, чипсы и крекеры")
В этом примере наряду с абстракцией используются концепции полиморфизма и наследования.
Наследование заключается в том, что подклассы Entree, Dessert и PartyMix наследуют абстрактный метод cook() от абстрактного базового класса Recipe. Это означает, что все они имеют ту же сигнатуру (название и параметры) метода cook(), что и абстрактный метод, определенный в классе Recipe.
Полиморфизм проявляется в том, что каждый подкласс класса Recipe реализует метод cook() по-разному. Например, Entree реализует cook() для вывода инструкций по приготовлению основного блюда на медленном огне, а Dessert реализует cook() для вывода инструкций по смешиванию ингредиентов десерта. Эта разница в реализации является примером полиморфизма, когда различные объекты могут рассматриваться как объекты, которые относятся к одному типу, но при этом ведут себя по-разному:
entree = Entree(["курица", "рис", "овощи"])
dessert = Dessert(["мороженое", "шоколадные чипсы", "мараскиновые вишни"])
partymix = PartyMix()
entree.cook()
dessert.cook()
partymix.cook()
Результат:
Готовим на медленном огне смесь ингредиентов (курица, рис, овощи) для основного блюда
Смешиваем мороженое, шоколадные чипсы, мараскиновые вишни для десерта
Готовим снеки - выкладываем на поднос орешки, чипсы и крекеры
Вызов метода cook() для подкласса Appetizer приведет к ожидаемой ошибке:
appetizer = Appetizer()
appetizer.cook()
Результат:
TypeError: Can't instantiate abstract class Appetizer with abstract methods cook
Полиморфизм
Полиморфизм позволяет обращаться с объектами разных классов так, как будто они являются объектами одного класса. Реализовать полиморфизм можно через наследование, интерфейсы и перегрузку методов. Этот подход имеет несколько весомых преимуществ:
- Позволяет использовать различные реализации методов в зависимости от типа объекта, что делает код более универсальным и удобным для использования.
- Уменьшает дублирование кода – можно написать одну функцию для работы с несколькими типами объектов.
- Позволяет использовать общие интерфейсы и абстракции для работы с объектами разных типов.
- Обеспечивает гибкость и расширяемость – можно добавлять новые типы объектов без необходимости изменять существующий код. Это дает возможность разработчикам встраивать новые функции в программу, не нарушая ее существующую функциональность.
Полиморфизм тесно связан с абстракцией:
- Абстракция позволяет скрыть детали реализации объекта и предоставить только необходимый интерфейс для работы с ним. Это помогает упростить код, сделать его более понятным и гибким.
- Полиморфизм предоставляет возможность использовать один и тот же интерфейс для работы с разными объектами, которые могут иметь различную реализацию. Этот подход значительно упрощает расширение функциональности ПО.
Таким образом, абстракция позволяет определить общий интерфейс для работы с объектами, а полиморфизм позволяет использовать этот интерфейс для работы с различными объектами, которые могут иметь различную реализацию.
Рассмотрим полиморфизм на примере класса Confectionary (кондитерские изделия):
class Confectionary:
def __init__(self, name, price):
self.name = name
self.price = price
def describe(self):
print(f"{self.name} по цене {self.price} руб/кг")
class Cake(Confectionary):
def describe(self):
print(f"{self.name} торт стоит {self.price} руб/кг")
class Candy(Confectionary):
def describe(self):
print(f"{self.name} конфеты стоимостью {self.price} руб/кг")
class Cookie(Confectionary):
pass
В этом примере мы определяем базовый класс Confectionary, который имеет атрибуты name и price, а также метод describe(). Затем мы определяем три подкласса класса Confectionary: Cake, Candy и Cookie. Cake и Candy переопределяют метод describe() своими собственными реализациями, которые включают тип кондитерского изделия (торт и конфеты соответственно), а Cookie наследует дефолтный метод describe() от Confectionary.
Если создать экземпляры этих классов и вызвать их методы describe(), можно убедиться, что результат зависит от реализации метода в каждом конкретном подклассе:
cake = Cake("Пражский", 1200)
candy = Candy("Шоколадные динозавры", 560)
cookie = Cookie("Овсяное печенье с миндалем", 250)
cake.describe()
candy.describe()
cookie.describe()
Вывод:
Пражский торт стоит 1200 руб/кг
Шоколадные динозавры конфеты стоимостью 560 руб/кг
Овсяное печенье с миндалем по цене 250 руб/кг
Теперь разберем на примере класса Beverage (напиток) взаимодействие полиморфизма с другими концепциями ООП. Beverage – родительский класс, который содержит:
- атрибуты названия, объема и цены;
- методы для получения и установки этих атрибутов;
- метод для вывода описания напитка.
Soda (газировка) – дочерний класс Beverage, у него есть дополнительный атрибут flavor (вкус) и собственный метод describe(), включающий flavor. DietSoda – еще один дочерний класс Soda, который наследует все атрибуты и методы Soda, но переопределяет метод describe(), чтобы указать, что газировка является диетической:
class Beverage:
def __init__(self, name, size, price):
self._name = name
self._size = size
self._price = price
def get_name(self):
return self._name
def get_size(self):
return self._size
def get_price(self):
return self._price
def set_price(self, price):
self._price = price
def describe(self):
return f'{self._size} л газировки "{self._name}" стоит {self._price} руб.'
class Soda(Beverage):
def __init__(self, name, size, price, flavor):
super().__init__(name, size, price)
self._flavor = flavor
def get_flavor(self):
return self._flavor
def describe(self):
return f'{self._size} л газировки "{self._name}" со вкусом "{self._flavor}" стоит {self._price} руб.'
class DietSoda(Soda):
def __init__(self, name, size, price, flavor):
super().__init__(name, size, price, flavor)
def describe(self):
return f'{self._size} л диетической газировки "{self._name}" со вкусом "{self._flavor}" стоит {self._price} руб.'
regular_soda = Soda('Sprite', 0.33, 45, 'лимон')
print(regular_soda.describe())
diet_soda = DietSoda('Mirinda', 0.33, 50, 'мандарин')
print(diet_soda.describe())
regular_soda = Soda('Буратино', 1.5, 65, 'дюшес')
print(regular_soda.describe())
Этот пример демонстрирует:
- Инкапсуляцию, поскольку атрибуты защищены символами подчеркивания и могут быть доступны только через методы getter и setter.
- Наследование, поскольку Soda и DietSoda наследуют атрибуты и метод от Beverage.
- Полиморфизм, поскольку каждый класс имеет свою собственную версию метода describe(), который возвращает различные результаты в зависимости от конкретного класса.
Вывод:
0.33 л газировки "Sprite" со вкусом "лимон" стоит 45 руб.
0.33 л диетической газировки "Mirinda" со вкусом "мандарин" стоит 50 руб.
1.5 л газировки "Буратино" со вкусом "дюшес" стоит 65 руб.
Отлично! Вы изучили два самых мощных принципа ООП — Абстракцию и Полиморфизм.
Теперь вы знаете теорию всех четырёх столпов объектно-ориентированного программирования и видели, как они работают вместе. У вас есть полный концептуальный каркас.
Но теория — это карта. Настоящее путешествие начинается, когда вы сами начинаете строить по ней. Готовы применить свои знания на практике? В полной версии урока вас ждут:
- 10 комплексных задач по ООП-дизайну, в которых вы с нуля спроектируете иерархии классов для самых разных сценариев: от видеоигр до CRM-систем.
- Реальная практика в применении всех четырёх принципов для создания чистого, модульного и расширяемого кода.
- Возможность научиться думать как архитектор ПО, а не только как кодер.
Комментарии
Задание 5, зачем нужен декоратор?
Спасибо большое за самоучитель! Очень помогло заново вспомнить питон спустя 2 года простоя и узнать много нового - до этого изучал его 2 года в Яндекс Лицее, поэтому могу сказать что курс хорош, но не для новичков. Как пример могу привести изучение генераторов с циклами, а уже потом циклов ( лично я вообще никогда не использовал генераторы, боюсь представить, что было бы с новичком, который только начал и уже видит такие длинные выражения в одну сроку, а только потом знакомится с циклами) От себя могу сказать, что довольно хорошо все написано, хоть и встречал немало опечаток. Было бы очень хорошим плюсом для данного самоучителя - побольше раскрыть условия заданий для практики, ибо очень часто непонятно: какой метод использовать, какие переменные, какие функции, как должны быть связаны данные, какие условия учитывать, а какие нет? В добавок сделать побольше разнообразия заданий, потому что часто повторяются, но с подкорректированным условием(как в данной теме). Вроде бы задания выстроены по сложности, но иногда бывает и такое, что в середине - сидишь много времени, а какое-нибудь 9 или 10 за мгновение делаешь. В заключении хочу еще раз сказать вам спасибо, мне понравилось изучать и практиковаться в этом курсе!
Если вы обнаружили опечатку, выделите ее и нажмите Ctrl+Enter.
Про использование методов, переменных и функций: дело в том, что любую задачу обычно можно решить несколькими совершенно разными способами, поэтому указание на какой-то конкретный метод решения может ограничивать решающего:). По этой причине для любых задач обычно указывают только примеры ввода и вывода, т.к. указание на учет/игнорирование каких-то условий может быть подсказкой. Насчет циклов и генераторов: это дело вкуса, я, например, обычные циклы не использую без крайней необходимости:). По поводу опечаток: буду рада исправить, если вы на них укажете ;).