Наталья Кайда 24 апреля 2023

🐍 Самоучитель по Python для начинающих. Часть 19: Основы ООП – абстракция и полиморфизм

Расскажем, для чего полиморфизм и абстракция используются в программировании, и как они взаимодействуют с остальными фундаментальными концепциями ООП. В конце статьи – решение 10 задач, связанных с абстрактными и полиморфными классами.
🐍 Самоучитель по Python для начинающих. Часть 19: Основы ООП – абстракция и полиморфизм

Напомним, что эта статья – продолжение темы ООП в 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 руб.

    

Практика

Задание 1

В далекой-далекой галактике Федерация ведет ожесточенную войну с клингонами. Звездолеты Федерации оснащены мощными фазерами, а клингонские корабли – смертоносными фотонными торпедами. Обе стороны разработали усовершенствованные варп-двигатели для перемещения со сверхсветовой скоростью, и оборудовали свои корабли системами самоуничтожения на случай чрезвычайной ситуации.

Для игры, посвященной этой войне, нужно создать абстрактный класс Starship с методами warp_speed(), fire_weapon() и self_destruct(). Кроме того, нужно создать два подкласса FederationStarship и KlingonWarship, которые наследуют абстрактные методы Starship и реализуют свои собственные версии методов warp_speed(), fire_weapon() и self_destruct().

Пример использования:

        enterprise = FederationStarship()
bird_of_prey = KlingonWarship()

enterprise.warp_speed()
bird_of_prey.warp_speed()

enterprise.fire_weapon()
bird_of_prey.fire_weapon()

enterprise.self_destruct()
bird_of_prey.self_destruct()

    

Вывод:

        Включить варп-двигатели!
Включить маскировочное устройство!
Выпустить фотонные торпеды!
Стрелять из фазеров!
Запускаю систему самоуничтожения...
Запускаю протокол самоуничтожения...

    

Решение:

        from abc import ABC, abstractmethod

class Starship(ABC):
    @abstractmethod
    def warp_speed(self):
        pass

    @abstractmethod
    def fire_weapon(self):
        pass

    @abstractmethod
    def self_destruct(self):
        pass

class FederationStarship(Starship):
    def warp_speed(self):
        print("Включить варп-двигатели!")

    def fire_weapon(self):
        print("Выпустить фотонные торпеды!")

    def self_destruct(self):
        print("Запускаю систему самоуничтожения...")

class KlingonWarship(Starship):
    def warp_speed(self):
        print("Включить маскировочное устройство!")

    def fire_weapon(self):
        print("Стрелять из фазеров!")

    def self_destruct(self):
        print("Запускаю протокол самоуничтожения...")

    

Задание 2

Для ПО ресторана нужно разработать модуль, помогающий контролировать использование фруктов и овощей на кухне. Создайте абстрактный класс Ingredient с методами get_name() и get_quantity(). Затем создайте два подкласса Vegetable и Fruit, которые наследуют абстрактные методы от Ingredient и реализуют свои собственные версии методов get_name() и get_quantity().

Пример использования:

        carrot = Vegetable("Морковь", 5)
apple = Fruit("Яблоки", 10)

print(carrot.get_name())
print(carrot.get_quantity())

print(apple.get_name())
print(apple.get_quantity())

    

Вывод:

        Морковь
5 кг
Яблоки
10 кг

    

Решение:

        from abc import ABC, abstractmethod

class Ingredient(ABC):
    @abstractmethod
    def get_name(self):
        pass

    @abstractmethod
    def get_quantity(self):
        pass

class Vegetable(Ingredient):
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    def get_name(self):
        return self.name

    def get_quantity(self):
        return f'{self.quantity} кг'

class Fruit(Ingredient):
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    def get_name(self):
        return self.name

    def get_quantity(self):
        return f'{self.quantity} кг'

    

Задание 3

Для военной стратегии необходимо создать абстрактный класс Soldier. Каждый солдат должен уметь двигаться, защищаться и атаковать, поэтому Soldier имеет три абстрактных метода: move(), attack() и defend(). Два конкретных класса, Infantry (пехота) и Cavalry (кавалерия), будут наследовать и реализовывать эти методы. В игре также должен быть класс Army, который будет добавлять солдат в армию и выполнять операции атаки и защиты.

Чтобы гарантировать, что используются только экземпляры класса Soldier, нужно создать декоратор validatesoldier, который будет проверять тип объекта. Если объект не является экземпляром класса Soldier, декоратор выдаст ошибку TypeError. Декоратор будет применяться к методам move(), attack() и defend() классов Infantry и Cavalry.

Пример использования:

        army = Army()
army.add_soldier(Infantry())
army.add_soldier(Cavalry())
army.add_soldier(Infantry())
army.add_soldier(Cavalry())

army.attack()
army.defend()

    

Вывод:

        Пехота передвигается в пешем порядке
Пехота участвует в ближнем бою
Кавалерия передвигается верхом
Кавалерия переходит в атаку
Пехота передвигается в пешем порядке
Пехота участвует в ближнем бою
Кавалерия передвигается верхом
Кавалерия переходит в атаку
Пехота передвигается в пешем порядке
Пехота держит строй
Кавалерия передвигается верхом
Кавалерия защищает фланги
Пехота передвигается в пешем порядке
Пехота держит строй
Кавалерия передвигается верхом
Кавалерия защищает фланги

    

Решение:

        from abc import ABC, abstractmethod

def validate_soldier(func):
    def wrapper(self):
        if not isinstance(self, Soldier):
            raise TypeError("Объект не является экземпляром Soldier")
        return func(self)
    return wrapper

class Soldier(ABC):
    @abstractmethod
    def move(self):
        pass

    @abstractmethod
    def attack(self):
        pass

    @abstractmethod
    def defend(self):
        pass

class Infantry(Soldier):
    @validate_soldier
    def move(self):
        print("Пехота передвигается в пешем порядке")

    @validate_soldier
    def attack(self):
        print("Пехота участвует в ближнем бою")

    @validate_soldier
    def defend(self):
        print("Пехота держит строй")

class Cavalry(Soldier):
    @validate_soldier
    def move(self):
        print("Кавалерия передвигается верхом")

    @validate_soldier
    def attack(self):
        print("Кавалерия переходит в атаку")

    @validate_soldier
    def defend(self):
        print("Кавалерия защищает фланги")

class Army:
    def __init__(self):
        self.soldiers = []

    def add_soldier(self, soldier):
        self.soldiers.append(soldier)

    def attack(self):
        for soldier in self.soldiers:
            soldier.move()
            soldier.attack()

    def defend(self):
        for soldier in self.soldiers:
            soldier.move()
            soldier.defend()

    

Задание 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) травоядных особей.

Пример использования:

        t_rex = Carnivore('Тираннозавр', 'Рекс', 4800, 560)
velociraptor = Carnivore('Велоцираптор', 'Зубастик', 30, 70)
stegosaurus = Herbivore('Стегозавр', 'Стегга', 7100, 420)
triceratops = Herbivore('Трицератопс', 'Трипси', 8000, 290)

park = DinosaurPark()

park.add_dinosaur(t_rex)
park.add_dinosaur(velociraptor)
park.add_dinosaur(stegosaurus)
park.add_dinosaur(triceratops)


for dinosaur in park.list_dinosaurs():
    print(f'Имя: {dinosaur[0]}\n'
    f'Вид: {dinosaur[1]}\n'
    f'Вес: {dinosaur[2]} кг\n'
    f'Рост: {dinosaur[3]} см\n'
    f'Рацион: {dinosaur[4]}\n')

    

Вывод:

        Имя: Рекс
Вид: Тираннозавр
Вес: 4800 кг
Рост: 560 см
Рацион: Плотоядный

Имя: Зубастик
Вид: Велоцираптор
Вес: 30 кг
Рост: 70 см
Рацион: Плотоядный

Имя: Стегга
Вид: Стегозавр
Вес: 7100 кг
Рост: 420 см
Рацион: Травоядный

Имя: Трипси
Вид: Трицератопс
Вес: 8000 кг
Рост: 290 см
Рацион: Травоядный

    

Решение:

        from abc import ABC, abstractmethod

class Dinosaur(ABC):
    @abstractmethod
    def get_personal_name(self):
        pass

    @abstractmethod
    def get_breed(self):
        pass

    @abstractmethod
    def get_height(self):
        pass

    @abstractmethod
    def get_weight(self):
        pass

    @abstractmethod
    def get_diet(self):
        pass

class Carnivore(Dinosaur):
    def __init__(self, breed, personal_name, height, weight):
        self.breed = breed
        self.personal_name = personal_name
        self.height = height
        self.weight = weight

    def get_personal_name(self):
        return self.personal_name

    def get_breed(self):
        return self.breed

    def get_height(self):
        return self.height

    def get_weight(self):
        return self.weight

    def get_diet(self):
        return 'Плотоядный'

class Herbivore(Dinosaur):
    def __init__(self, breed, personal_name, height, weight):
        self.breed = breed
        self.personal_name = personal_name
        self.height = height
        self.weight = weight

    def get_personal_name(self):
        return self.personal_name

    def get_breed(self):
        return self.breed

    def get_height(self):
        return self.height

    def get_weight(self):
        return self.weight

    def get_diet(self):
        return 'Травоядный'

class DinosaurPark:
    def __init__(self):
        self.dinosaurs = []

    def add_dinosaur(self, dinosaur):
        self.dinosaurs.append(dinosaur)

    
    def list_dinosaurs(self):
        return [(dinosaur.get_personal_name(), dinosaur.get_breed(), dinosaur.get_height(), dinosaur.get_weight(), dinosaur.get_diet()) for dinosaur in self.dinosaurs]

    
    def list_carnivores(self):
        return [(dinosaur.get_personal_name(), dinosaur.get_breed(), dinosaur.get_height(), dinosaur.get_weight()) for dinosaur in self.dinosaurs if isinstance(dinosaur, Carnivore)]

    
    def list_herbivores(self):
        return [(dinosaur.get_personal_name(), dinosaur.get_breed(), dinosaur.get_height(), dinosaur.get_weight()) for dinosaur in self.dinosaurs if isinstance(dinosaur, Herbivore)]

    

Задание 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) струнных.

Пример использования:

        chello = StringedInstrument("виолончель", "струнный инструмент", "Strum")
maracas = PercussionInstrument("маракасы", "ударный инструмент", "Maracas")
violin = StringedInstrument("скрипка", "струнный инструмент", "Virtuso")
drums = PercussionInstrument("барабан", "ударный инструмент", "Beat")

orchestra = Orchestra()
orchestra.add_instrument(chello)
orchestra.add_instrument(maracas)
orchestra.add_instrument(violin)
orchestra.add_instrument(drums)

print("В оркестрe есть инструменты:", ', '.join(orchestra.list_instruments()))  
print("Струнные инструменты:", ', '.join(orchestra.list_stringed_instruments())) 
print("Ударные инструменты:", ', '.join(orchestra.list_percussion_instruments()))  

print(chello.play())    
print(drums.play())

    

Вывод:

        В оркестрe есть инструменты: виолончель, маракасы, скрипка, барабан
Струнные инструменты: виолончель, скрипка
Ударные инструменты: маракасы, барабан
Звучит струнный инструмент виолончель
Звучит ударный инструмент барабан

    

Решение:

        from abc import ABC, abstractmethod

def print_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return wrapper

class Instrument(ABC):
    @abstractmethod
    def get_name(self):
        pass

    @abstractmethod
    def get_type(self):
        pass

    @abstractmethod
    def get_sound(self):
        pass

    @abstractmethod
    def play(self):
        pass

class StringedInstrument(Instrument):
    def __init__(self, name, type, sound):
        self.name = name
        self.type = type
        self.sound = sound

    def get_name(self):
        return self.name

    def get_type(self):
        return self.type

    def get_sound(self):
        return self.sound


    def play(self):
        return f"Звучит {self.type} {self.name}"

class PercussionInstrument(Instrument):
    def __init__(self, name, type, sound):
        self.name = name
        self.type = type
        self.sound = sound

    def get_name(self):
        return self.name

    def get_type(self):
        return self.type

    def get_sound(self):
        return self.sound

    def play(self):
        return f"Звучит {self.type} {self.name}"

class Orchestra:
    def __init__(self):
        self.instruments = []

    def add_instrument(self, instrument):
        self.instruments.append(instrument)

    @print_decorator
    def list_instruments(self):
        return [instrument.get_name() for instrument in self.instruments]

    @print_decorator
    def list_stringed_instruments(self):
        return [instrument.get_name() for instrument in self.instruments if isinstance(instrument, StringedInstrument)]

    @print_decorator
    def list_percussion_instruments(self):
        return [instrument.get_name() for instrument in self.instruments if isinstance(instrument, PercussionInstrument)]

    

Задание 6

Напишите класс FilmCatalogue (каталог фильмов), который отвечает за ведение фильмотеки. FilmCatalogue должен поддерживать различные типы кинокартин, чтобы пользователи могли искать фильмы по определенному жанру. Для этого необходимо создать новые классы для различных жанров (например, Horror, Action, Romance), которые наследуют класс Movie и переопределяют метод play() для вывода информации о том, к какому жанру относится фильм.

Пример использования:

        my_catalogue = FilmCatalogue()

my_catalogue.add_movie(Drama("Крестный отец", "Френсис Ф. Коппола"))
my_catalogue.add_movie(Comedy("Ночные игры", "Джон Фрэнсис Дейли, Джонатан М. Голдштейн"))
my_catalogue.add_movie(Horror("Дракула Брэма Стокера", "Френсис Ф. Коппола"))
my_catalogue.add_movie(Action("Крушение", "Жан-Франсуа Рише"))
my_catalogue.add_movie(Romance("Честная куртизанка", "Маршалл Херсковиц"))

my_catalogue.play_all_movies()

print(f"\nНайдены фильмы ужасов:")
for movie in my_catalogue.search_movies_by_genre(Horror):
    print(movie.title)

print(f"\nЗапускаем фильм из жанра 'Мелодрамы':")
my_catalogue.play_movies_by_genre(Romance)

    

Вывод:

        Включаем драму 'Крестный отец' реж. Френсис Ф. Коппола.
Включаем комедию 'Ночные игры' реж. Джон Фрэнсис Дейли, Джонатан М. Голдштейн.
Включаем фильм ужасов 'Дракула Брэма Стокера' реж. Френсис Ф. Коппола.
Включаем боевик 'Крушение' реж. Жан-Франсуа Рише.
Включаем мелодраму 'Честная куртизанка' реж. Маршалл Херсковиц.

Найдены фильмы ужасов:
Дракула Брэма Стокера

Запускаем фильм из жанра 'Мелодрамы':
Включаем мелодраму 'Честная куртизанка' реж. Маршалл Херсковиц.

    

Решение:

        class Movie:
    def __init__(self, title, director):
        self.title = title
        self.director = director

    def play(self):
        print(f"Собираемся смотреть фильм '{self.title}' реж. {self.director}.")


class Comedy(Movie):
    def play(self):
        print(f"Включаем комедию '{self.title}' реж. {self.director}.")


class Drama(Movie):
    def play(self):
        print(f"Включаем драму '{self.title}' реж. {self.director}.")


class Horror(Movie):
    def play(self):
        print(f"Включаем фильм ужасов '{self.title}' реж. {self.director}.")


class Action(Movie):
    def play(self):
        print(f"Включаем боевик '{self.title}' реж. {self.director}.")


class Romance(Movie):
    def play(self):
        print(f"Включаем мелодраму '{self.title}' реж. {self.director}.")


class FilmCatalogue:
    def __init__(self):
        self.movies = []

    def add_movie(self, movie):
        self.movies.append(movie)

    def play_all_movies(self):
        for movie in self.movies:
            movie.play()

    def search_movies_by_genre(self, genre):
        return [movie for movie in self.movies if isinstance(movie, genre)]

    def play_movies_by_genre(self, genre):
        movies = self.search_movies_by_genre(genre)
        for movie in movies:
            movie.play()

    

Задание 7

Для CRM винодельни нужно написать модуль, отвечающий за учет красных, белых и розовых вин, каждое из которых имеет свое название, сорт винограда, год и температуру подачи. Создайте базовый класс Wine с атрибутами name, grape и year. Затем создайте три подкласса RedWine, WhiteWine и RoseWine, которые наследуют методы и атрибуты от Wine и реализуют свои собственные версии метода serve(). Кроме того, создайте класс Winery, который ведет список вин и имеет метод serve_wines(), вызывающий метод serve() для каждого вина.

Пример использования:

        winery = Winery()
winery.add_wine(RedWine("Cabernet Sauvignon", "Каберне Совиньон", 2015))
winery.add_wine(WhiteWine("Chardonnay", "Шардоне", 2018))
winery.add_wine(RoseWine("Grenache", "Гренаш", 2020))
winery.serve_wines()

    

Вывод:

        Красное вино 'Cabernet Sauvignon', сделанное из винограда сорта Каберне Совиньон в 2015 году, рекомендуем подавать комнатной температуры.
Белое вино 'Chardonnay', сделанное из винограда сорта Шардоне в 2018 году, рекомендуем подавать хорошо охлажденным.
Розовое вино 'Grenache', сделанное из винограда сорта Гренаш в 2020 году, рекомендуем подавать слегка охлажденным.

    

Решение:

        class Wine:
    def __init__(self, name, grape, year):
        self.name = name
        self.grape = grape
        self.year = year

class RedWine(Wine):
    def serve(self):
        print(f"Красное вино '{self.name}', сделанное из винограда сорта {self.grape} в {self.year} году, рекомендуем подавать комнатной температуры.")

class WhiteWine(Wine):
    def serve(self):
        print(f"Белое вино '{self.name}', сделанное из винограда сорта {self.grape} в {self.year} году, рекомендуем подавать хорошо охлажденным.")

class RoseWine(Wine):
    def serve(self):
        print(f"Розовое вино '{self.name}', сделанное из винограда сорта {self.grape} в {self.year} году, рекомендуем подавать слегка охлажденным.")

class Winery:
    def __init__(self):
        self.wines = []

    def add_wine(self, wine):
        self.wines.append(wine)

    def serve_wines(self):
        for wine in self.wines:
            wine.serve()

    

Задание 8

Для ПО аэропорта нужно разработать модуль, отслеживающий пассажирские и грузовые самолеты, которые отличаются моделью, производителем, вместимостью и грузоподъемностью. Создайте базовый класс Aircraft (воздушное судно) с атрибутами model, manufacturer и capacity. Затем создайте два подкласса PassengerAircraft и CargoAircraft, которые наследуют атрибуты и методы от Aircraft и реализуют свои собственные версии метода fly(). В дополнение создайте класс Airport, который содержит список самолетов и имеет метод takeoff(), вызывающий метод fly() для каждого самолета.

Пример использования:

        airport = Airport()
airport.add_aircraft(PassengerAircraft("Boeing 747", "Боинг", 416))
airport.add_aircraft(CargoAircraft("Airbus A330", "Эйрбас", 70))
airport.add_aircraft(PassengerAircraft("Boeing 777", "Боинг", 396))
airport.takeoff()

    

Вывод:

        Пассажирский самолет 'Boeing 747' вместимостью 416 человек, произведенный компанией Боинг, поднимается в воздух с пассажирами на борту.
Грузовой самолет 'Airbus A330' с грузоподъемностью 70 т, произведенный компанией Эйрбас, поднимается в воздух с грузом на борту.
Пассажирский самолет 'Boeing 777' вместимостью 396 человек, произведенный компанией Боинг, поднимается в воздух с пассажирами на борту.

    

Решение:

        class Aircraft:
    def __init__(self, model, manufacturer, capacity):
        self.model = model
        self.manufacturer = manufacturer
        self.capacity = capacity

class PassengerAircraft(Aircraft):
    def fly(self):
        print(f"Пассажирский самолет '{self.model}' вместимостью {self.capacity} человек, произведенный компанией {self.manufacturer}, поднимается в воздух с пассажирами на борту.")

class CargoAircraft(Aircraft):
    def fly(self):
        print(f"Грузовой самолет '{self.model}' с грузоподъемностью {self.capacity} т, произведенный компанией {self.manufacturer}, поднимается в воздух с грузом на борту.")

class Airport:
    def __init__(self):
        self.aircrafts = []

    def add_aircraft(self, aircraft):
        self.aircrafts.append(aircraft)

    def takeoff(self):
        for aircraft in self.aircrafts:
            aircraft.fly()

    

Задание 9

Необходимо реализовать модуль, отвечающий за обработку данных о тестировании конфигурации настольных компьютеров и ноутбуков, каждый из которых отличается моделью, процессором, памятью и производительностью. Создайте базовый класс Computer с атрибутами model, processor и memory. Затем создайте два подкласса Desktop и Laptop, которые наследуют атрибуты и методы Computer и реализуют свои собственные версии метода run(). В дополнение, создайте класс ComputerStore, который содержит список компьютеров и имеет метод run_tests(), вызывающий метод run() для каждого компьютера. Используйте декораторы для вывода результатов.

Пример использования:

        store = ComputerStore()
store.add_computer(Desktop("HP Legion", "Intel Core i9-10900K", "64 Гб"))
store.add_computer(Laptop("Dell Xtra", "Intel Core i5 13600K", "32 Гб"))
store.add_computer(Desktop("Lenovo SuperPad", "AMD Ryzen 7 2700X", "16 Гб"))
store.run_tests()

    

Вывод:

        Начинаем тест производительности...
Запускаем настольный компьютер 'HP Legion' с процессором Intel Core i9-10900K и 64 Гб RAM.
Тест производительности завершен.
Начинаем тест производительности...
Запускаем ноутбук 'Dell Xtra' с процессором Intel Core i5 13600K и 32 Гб RAM.
Тест производительности завершен.
Начинаем тест производительности...
Запускаем настольный компьютер 'Lenovo SuperPad' с процессором AMD Ryzen 7 2700X и 16 Гб RAM.
Тест производительности завершен.

    

Решение:

        def performance_test(func):
    def wrapper(*args, **kwargs):
        print("Начинаем тест производительности...")
        result = func(*args, **kwargs)
        print("Тест производительности завершен.")
        return result
    return wrapper

class Computer:
    def __init__(self, model, processor, memory):
        self.model = model
        self.processor = processor
        self.memory = memory

    @performance_test
    def run(self):
        pass

class Desktop(Computer):
    @performance_test
    def run(self):
        print(f"Запускаем настольный компьютер '{self.model}' с процессором {self.processor} и {self.memory} RAM.")

class Laptop(Computer):
    @performance_test
    def run(self):
        print(f"Запускаем ноутбук '{self.model}' с процессором {self.processor} и {self.memory} RAM.")

class ComputerStore:
    def __init__(self):
        self.computers = []

    def add_computer(self, computer):
        self.computers.append(computer)

    def run_tests(self):
        for computer in self.computers:
            computer.run()

    

Задание 10

Определите базовый класс Cryptocurrency, имеющий атрибуты:

  • name – название;
  • symbol – символ-тикер;
  • minable – возможность добычи майнингом;
  • rate_to_usd – текущий курс к доллару;
  • anonymous – наличие анонимных транзакций.

Затем создайте три подкласса Nano, Iota и Stellar, которые наследуют атрибуты и методы родительского класса Cryptocurrency, и обладают дополнительными свойствами, влияющими на размер вознаграждения за майнинг:

  • атрибут block_lattice у Nano;
  • tangle у Iota.

Кроме того, нужно реализовать:

  1. Декоратор minable_required, который проверяет, можно ли майнить криптовалюту перед вызовом метода mining_reward(), и выводит сообщение, если ее майнить нельзя.
  2. Функцию print_info, которая принимает на вход экземпляр криптовалюты и выводит информацию о монете, включая название, символ, возможность добычи, курс к доллару США, анонимность и наличие блок-решетки.

Пример использования:

        cryptocurrencies = [Nano(block_lattice=True, rate_to_usd=6, anonymous=False),
                    Iota(tangle=True, rate_to_usd=0.4, anonymous=False),
                    Stellar(distributed=False, rate_to_usd=0.15, anonymous=True)]

for crypto in cryptocurrencies:
    print_info(crypto)
    if crypto.minable:
        print(f"Награда за майнинг: {crypto.mining_reward()} {crypto.symbol}\n")

    

Вывод:

        Nano (NANO): добывают майнингом, курс к USD: 6, только публичные транзакции, блок-решетка
Награда за майнинг: 0.02 NANO

Iota (IOTA): добывают майнингом, курс к USD: 0.4, только публичные транзакции
Награда за майнинг: 0.001 IOTA

Stellar (XLM): не майнится, курс к USD: 0.15, анонимные транзакции

    

Решение:

        class Cryptocurrency:
    def __init__(self, name, symbol, minable=True, rate_to_usd=1, anonymous=False):
        self.name = name
        self.symbol = symbol
        self.minable = minable
        self.rate_to_usd = rate_to_usd
        self.anonymous = anonymous

    def __str__(self):
        return f"{self.name} ({self.symbol})"

    def mining_reward(self):
        return None

def minable_required(func):
    def wrapper(crypto):
        if not crypto.minable:
            print("Эту криптовалюту не майнят")
            return None
        return func(crypto)
    return wrapper

class Nano(Cryptocurrency):
    def __init__(self, block_lattice=False, rate_to_usd=5, anonymous=True):
        super().__init__("Nano", "NANO", True, rate_to_usd, anonymous)
        self.block_lattice = block_lattice

    @minable_required
    def mining_reward(self):
        return 0.02 if self.block_lattice else 0.01

class Iota(Cryptocurrency):
    def __init__(self, tangle=False, rate_to_usd=0.3, anonymous=False):
        super().__init__("Iota", "IOTA", True, rate_to_usd, anonymous)
        self.tangle = tangle

    @minable_required
    def mining_reward(self):
        return 0.001 if self.tangle else 0.002

class Stellar(Cryptocurrency):
    def __init__(self, distributed=False, rate_to_usd=0.1, anonymous=True):
        super().__init__("Stellar", "XLM", False, rate_to_usd, anonymous)
        self.distributed = distributed

    def mining_reward(self):
        print("Stellar is not minable")
        return None

def print_info(crypto):
    minable_str = 'добывают майнингом' if crypto.minable else 'не майнится'
    anonymity_str = 'анонимные транзакции' if crypto.anonymous else 'только публичные транзакции'
    block_lattice_str = 'блок-решетка' if hasattr(crypto, 'block_lattice') and crypto.block_lattice else ''
    
    if block_lattice_str:
        print(f"{crypto}: {minable_str}, курс к USD: {crypto.rate_to_usd}, {anonymity_str}, {block_lattice_str}")
    else:
        print(f"{crypto}: {minable_str}, курс к USD: {crypto.rate_to_usd}, {anonymity_str}")

    

Подведем итоги

Объектно-ориентированный подход обладает несколькими важными преимуществами, среди которых:

  • Модульность – ООП позволяет разбивать сложные задачи на более мелкие и простые составляющие.
  • Возможность переиспользования можно многократно использовать однажды написанный код с помощью наследования и полиморфизма.
  • Инкапсуляция – можно скрыть детали реализации одних модулей от других частей программы. Это помогает сделать код более безопасным, поскольку другие части вашей программы не могут получить доступ или изменить данные или методы, которые не предназначены для использования извне.
  • Гибкость – ООП позволяет легко создавать новые объекты и изменять существующие. Можно добавлять или удалять методы и свойства по мере необходимости, а также создавать новые объекты, которые наследуют атрибуты и методы существующих классов.
  • Простота сопровождения и отладки. Разделение код на составляющие модули упрощает структуру программы и помогает изолировать ошибки. Кроме того, использование описательных имен классов и методов делает код более читабельным и понятным.

В следующей части будем изучать основы разработки приложений с графическим интерфейсом.

***

Содержание самоучителя

  1. Особенности, сферы применения, установка, онлайн IDE
  2. Все, что нужно для изучения Python с нуля – книги, сайты, каналы и курсы
  3. Типы данных: преобразование и базовые операции
  4. Методы работы со строками
  5. Методы работы со списками и списковыми включениями
  6. Методы работы со словарями и генераторами словарей
  7. Методы работы с кортежами
  8. Методы работы со множествами
  9. Особенности цикла for
  10. Условный цикл while
  11. Функции с позиционными и именованными аргументами
  12. Анонимные функции
  13. Рекурсивные функции
  14. Функции высшего порядка, замыкания и декораторы
  15. Методы работы с файлами и файловой системой
  16. Регулярные выражения
  17. Основы скрапинга и парсинга
  18. Основы ООП: инкапсуляция и наследование
  19. Основы ООП: абстракция и полиморфизм
  20. Графический интерфейс на Tkinter
  21. Основы разработки игр на Pygame
  22. Основы работы с SQLite
  23. Основы веб-разработки на Flask
  24. Основы работы с NumPy
  25. Основы анализа данных с Pandas

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ