Python

Python. Классы.

Классы в Python позволяют придерживаться подхода ООП (объектно-ориентированное программирование). Суть этого подходя заключается в том, что программы строятся на основе взаимодействия объектов, а не последовательного выполнения инструкций. Такие программы состоят из набора объектов, каждый из которых имеет свои характеристики (атрибуты) и действия (методы), и эти объекты взаимодействуют друг с другом, чтобы выполнить задачу. Например, объект “машина” может иметь атрибуты “Гос номер”, “Цвет”, “Год выпуска” и методы “Ехать”, “Тормозить”, “Повернуть”.

Чтобы лучше понять преимущества использования объектов, давайте рассмотрим такую задачу. Предположим, мы разрабатываем интернет-магазин компьютерной техники. Нам нужно показать посетителю список товаров. Без использования объектного подхода, мы были бы вынуждены создать отдельную переменную для каждой характеристики каждого товара. Например:

item1_name = "Samsung S3"
item1_price = 10399
item1_diagonal = 24

item2_name = "LG 27MR400"
item2_price = 10499
item2_diagonal = 27

Объектный подход позволяет сгруппировать характеристики каждого товара:

item1.name = "Samsung S3"
item1.price = 10399
item1.diagonal = 24

item2.name = "LG 27MR400"
item2.price = 10499
item2.diagonal = 27

Здесь у нас есть два объекта товара – item1 и item2. У каждого из них есть свои характеристики (или атрибуты) – название, категория и цена.

Объявляем класс

Класс можно рассматривать как шаблон, на основе которого создаются объекты. В классе мы можем указать, какими характеристиками будут обладать все объекты данного класса. Вот как можно определить собственный класс на примере товара “монитор” в интернет-магазине:

class Monitor:
    def __init__(self, name, price, diagonal):
        self.name = name
        self.price = price
        self.diagonal = diagonal

Здесь class – это ключевое слово, которое говорит, что дальше будет идти определение класса. Monitor – название нашего класса. Дальше мы определяем конструктор с помощью функции со служебным названием __init__. При помощи конструктора создаются объекты (экземпляры класса). Можно представлять конструктор как фабрику объектов, которая инициализирует атрибуты. Первым параметром конструктор принимает значение self и это ссылка на сам экземпляр класса. Через последующие параметры конструктора мы можем передать значения, необходимые для инициализации атрибутов экземпляра. В теле конструктора мы просто сохраняем значения параметров в атрибутах переменной self.

Теперь давайте посмотрим, как создать экземпляр нашего класса Monitor:

item1 = Monitor("Samsung S3", 10399, 24)
item2 = Monitor("LG 27MR400", 10499, 27)

То есть мы просто используем название класса, как будто вызываем функцию и передаем в нее аргументы для каждого параметра функции __init__ (за исключением значения для self, которое передается автоматически).

Теперь мы можем обращаться к атрибутам объекта с помощью точки и имени атрибута:

print(item1.name, item1.price, sep=" - ")  # Samsung S3 - 10399

Атрибуты представляют собой обычные переменные, прикрепленные к объекту. Из значения можно изменить, как мы это делаем с обычными переменными:

item1.price = 9999
print(item1.name, item1.price, sep=" - ")  # Samsung S3 - 9999

item2.price -= 200
print(item2.name, item2.price, sep=" - ")  # LG 27MR400 – 10299

Добавляем действия

Мы также можем определить функции, которые можно будет вызвать для любого экземпляра класса. Функции, определенные для класса, называют методами. Вы уже могли сталкиваться с методами – вспомните методы isdigit, upper и split у строк.

Давайте добавим метод для формирования описания товара:

class Monitor:
    def __init__(self, name, price, diagonal):
        self.name = name
        self.price = price
        self.diagonal = diagonal
    def get_description(self):
        return f"Монитор {self.name} \nДиагональ: {self.diagonal}\" \nЦена: {self.price} руб."

item3 = Monitor("ASUS VY279HF", 13499, 27)
print(item3.get_description())
# Монитор ASUS VY279HF
# Диагональ: 27"
# Цена: 13499 руб.

Как и конструктор, методы класса принимают self первым параметром, через который мы можем получить доступ к атрибутам и методам данного экземпляра класса. Аргумент self не нужно явно передавать при вызове метода, это делается автоматически.

Методы могут так же изменять значения атрибутов экземпляра. Добавим метод, который меняет цену монитора на заданный процент:

class Monitor:
    def __init__(self, name, price, diagonal):
        self.name = name
        self.price = price
        self.diagonal = diagonal
    def get_description(self):
        return f"Монитор {self.name} \nДиагональ: {self.diagonal}\" \nЦена: {self.price} руб."
    def change_price(self, percentage):
        self.price += (percentage / 100) * self.price

item4 = Monitor("MSI PRO MP273QW", 16099, 27)
item4.change_price(10)
print(item4.get_description())
# Монитор MSI PRO MP273QW
# Диагональ: 27"
# Цена: 17708.9 руб.

Наследование

Теперь давайте представим, что наш интернет-магазин кроме мониторов продает еще жесткие диски. У жестких дисков скорее всего будут другие характеристики, хотя некоторые (название, цена) также присутствуют.

Для начала добавим класс жесткого диска:

class HardDrive:
    def __init__(self, name, price, type, memory):
        self.name = name
        self.price = price
        self.type = type
        self.memory = memory
    def get_description(self):
        return f"Жесткий диск {self.name} \nТип: {self.type}\" \nОбъем: {self.memory} ГБ \nЦена: {self.price} руб."
    def change_price(self, percentage):
        self.price += (percentage / 100) * self.price

И создадим несколько объектов этого класса:

item5 = HardDrive("ADATA HV620 Slim", 4799, "HDD", 1000)
item6 = HardDrive("HP P500", 2450, "SSD", 120)

Мы можем вызвать метод get_description также, как делали это для мониторов:

print(item5.get_description())
# Жесткий диск ADATA HV620 Slim
# Тип: HDD
# Объем: 1000 ГБ
# Цена: 4799 руб.

Между классами Monitor и HardDrive можно заметить сходство. Оба этих класса содержат атрибуты name и price, а также абсолютно одинаковые методы change_price. Кроме того – и мониторы и жесткие диски являются товарами в нашем интернет-магазине. Для того, чтобы избавиться от дублирования кода, а также, чтобы подчеркнуть принадлежность мониторов и жестких дисков к товарам – мы воспользуемся наследованием.

Один класс может наследоваться от другого. Экземпляры дочернего класса, кроме собственных атрибутов и методов, будут обладать всеми атрибутами и методами родительского класса. Создадим родительский класс для всех товаров:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def get_description(self):
        pass
    def change_price(self, percentage):
        self.price += (percentage / 100) * self.price

У этого класса есть атрибуты название и цена, метод изменения цены и метод получения описания товара. Ключевым словом pass в теле метода get_description мы говорим, что базовый класс Product не знает, как сформировать описание товара, поэтому он просто ничего не будет делать. Для каждого дочернего класса мы реализуем собственный метод get_description.

Теперь скорректируем классы Monitor и HardDrive, чтобы они наследовались от класса Product:

class Monitor(Product):
    def __init__(self, name, price, diagonal):
        super().__init__(name, price)
        self.diagonal = diagonal
    def get_description(self):
        return f"Монитор {self.name} \nДиагональ: {self.diagonal}\" \nЦена: {self.price} руб."

class HardDrive(Product):
    def __init__(self, name, price, type, memory):
        super().__init__(name, price)
        self.type = type
        self.memory = memory
    def get_description(self):
        return f"Жесткий диск {self.name} \nТип: {self.type} \nПамять: {self.memory} ГБ\nЦена: {self.price} руб."

После названия класса мы в скобках указали название родительского класса. Это значит, что мы наследуем все атрибуты и методы от указанного класса.

В конструкторах дочерних классов первым делом мы использовали конструкцию super().__init__(name, price). Таким способом, мы вызываем конструктор родительского класса (его также называют суперклассом, отсюда название функции super). То есть функция super() дает нам доступ к родительскому классу и для него мы вызываем конструктор __init__, передавая аргументы, которые он ожидает (название товара и цена). После этого в конструкторе происходит сохранение “собственных” атрибутов. Метод get_description в каждом дочернем классе переопределен и формирует описание так, как нам нужно для каждого типа товара.

Теперь мы можем создать список товаров и вывести описание каждого из них таким образом:

# Указываем, что products будет содержать список товаров - мониторы или жесткие диски
products: list[Product] = []

# Добавляем объекты в список товаров
products.append(Monitor("Samsung S3", 10399, 24))
products.append(HardDrive("ADATA HV620 Slim", 4799, "HDD", 1000))
products.append(Monitor("LG 27MR400", 10499, 27))
products.append(HardDrive("HP P500", 2450, "SSD", 120))

# Python знает что у всех объектов товаров есть метод get_description
for item in products:
    print(item.get_description(), end="\n---\n")

Запустите этот код и убедитесь, что для разных типов товара формируется разное описание.

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

Итоги

Мы рассмотрели классы – основной инструмент для реализации ООП. В качестве преимуществ ООП можно выделить:

  • Структурированность – помогает организовать код в логические блоки, что делает его более понятным и удобным для поддержки.
  • Повторное использование кода – наследование позволяет повторно использовать код, что экономит время и усилия.
  • Модульность – программы легче разрабатывать и тестировать, так как каждый объект работает автономно.
  • Масштабируемость – легче добавлять новые функции и расширять функциональность программы.

Используя ООП, вы сможете разрабатывать сложные приложения, при этом код будет более организованным, модульным и удобным для поддержки и расширения.