К этому моменту вы уже освоили основные инструменты Python: типа данных, условия, циклы и функции. Вы научились писать программы, которые решают вполне конкретные задачи, например, обрабатывают список чисел или создают словарь. Это основа, которая позволяет создавать простые, но полезные программы.

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

Тогда характеристики автомобиля будем хранить в словаре:

lada_granta = {
    "Модель": "Lada Granta Седан", 
    "Цена": 800_000, 
    "Наличие": True
}
audi_a3 = {
    "Модель": "Audi A3 III", 
    "Цена": 1_500_000, 
    "Наличие": True
}

А действия, совершаемые над ним, напишем в виде отдельных функций. Пусть функция aplly_dicsount() принимает словарь car с данными об автомобиле и скидку percentage в процентах, а возвращает новую цену:

def apply_discount(car: dict, percentage: int) -> int | float:
    discount_amount = car["Цена"] * (percentage / 100)
    new_price = car["Цена"] - discount_amount
    print(f"Новая цена автомобиля {car["Модель"]}: {new_price} руб.")
    return new_price

А функция продажи автомобиля sell() также принимает словарь car и изменяет и возвращает статус наличия автомобиля:

def sell(car: dict) -> bool:
    if car["Наличие"]:
        print(f"Автомобиль {car['Модель']} успешно продан")
        return False

    print(f"Автомобиль {car["Модель"]} уже был продан")
    return True

В таком случае для совершения действий над автомобилем следует не только вызывать функции, но и обновлять данные в словаре:

lada_granta["Цена"] = apply_discount(lada_granta, 10)
# Вывод: Новая цена автомобиля Lada Granta Седан: 720000.0 руб. 
lada_granta["Наличие"] = sell(lada_granta)
# Вывод: Автомобиль Audi A3 III успешно продан

Мы с вами уже не раз писали код в таком стиле, и, по сути, следовали парадигме процедурного программирования, когда программа основана на последовательном вызове функций (процедур), но при этом данные и функции существуют отдельно друг друга.

Робот Кеша читает

Слово «парадигма» (от греч. paradeigma – шаблон, пример) в программировании означает определенный стиль или подход к написанию кода. Это как разные философские школы: каждая предлагает свой способ мышления и решения задачи. Другими словами, в программировании парадигма – это концептуальная модель, которая определяет, как будет строиться ваша программа. Если вы пишете код в процедурном стиле, вы думаете о нем как о серии шагов.

Но что, если мы хотим объединить и значения, и функции для работы с ними? Ведь было бы намного удобнее объединить характеристики автомобиля и действия над ним в один объект, а не скрупулёзно отслеживать обновление данных в словаре.

И это возможно, если мы обратимся к концепции объектно-ориентированного программирования (ООП), одним из основных принципов которого является объединение данных и функций в единое целое – объект.

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

Классы и объекты

Любой объект создаётся на основе класса, который является шаблоном, определяющим свойства и поведение объекта. Поэтому такой объект ещё называют экземпляром класса. Может быть вы помните, что функция type() возвращает конструкцию class <тип>. Это связано с тем, что все встроенные типы данных являются классами. Например, строка "Привет!" является объектом или экземпляром класса str и поддерживает всего его свойства и методы.

И мы можем не только использовать встроенные классы, но и создавать свои собственные. Давайте создадим класс для автомобиля в автосалоне. Для этого нам понадобится ключевое слово class, за которым следует имя класса, написанное с большой буквы. Если названия переменных и функций мы писали в «змеином регистре» snake_case, то классы принято именовать в «верблюжьем регистре» CamelCase. То есть каждое слово начинается с заглавной буквы и без разделителей следует за предыдущим:

class Car:
    pass

Здесь мы создали пустой класс Car с помощью уже знакомого нам ключевого слова pass.

Сам по себе класс – это просто шаблон. Для того чтобы с ним работать, следует создать его объект, вызвав имя класса, за которым следуют круглые скобки:

lada_granta = Car()

Это выражение создаёт новый объект класса Car() и присваивает его переменной lada_granta. Мы можем убедиться в этом с помощью функции type():

print(type(lada_granta))
# Вывод: <class '__main__.Car'>

Атрибуты и методы объекта

Однако для полноценной работы нам потребуются атрибуты и методы экземпляра класса.

Атрибуты объекта и метод __init__()

Атрибуты объекта – это переменные, предназначенные для хранения данных о каждом конкретном объекте класса. В нашем примере с автомобилем атрибутами должны быть переменные model для хранения модели, price – для цены и in_stock – для статуса наличия автомобиля на складе.

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

lada_granta.model = "Lada Granta Седан"
lada_granta.price = 800_000
lada_granta.in_stock = True

Здесь для объекта lada_granta были созданы новые атрибут model, price и in_stock.

Также при создании объекта Python создает словарь __dict__, в который добавляет все атрибуты этого объекта:

print(lada_granta.__dict__)
# Вывод: {'model': 'Lada Granta Седан', 'price': 800000, 'in_stock': True}

Для того, чтобы задать начальные значения атрибутов объекта нам необходим магический метод __init__(), называемый конструктором класса. Название init (от англ. initialize – инициализировать) говорит само за себя – его основной задачей является инициализировать атрибуты нового объекта, то есть задать их начальные значения.

Робот Кеша читает

Методы, которые позволяют настраивать встроенное поведение класса, принято называть магическими методами или dunder-методами (от англ. double underscores – двойное нижнее подчёркивание), так как они всегда начинаются и заканчиваются двумя нижними подчёркиваниями. Такие методы вызываются автоматически интерпретатором Python в ответ на определённые события. Например, метод __init__() магически вызывается при создании объекта класса.

Первым параметром любого метода объекта, в том числе метода __init__(), всегда должна быть ссылка на конкретный объект, для которого вызывается метод. Её принято обозначать с помощью слова self, однако это не ключевое слово, как def или class, а скорее общепринятое соглашение об именовании, которого придерживаются все разработчики Python.

Параметр self позволяет обращаться ко всем атрибутам и методам объекта, в том числе, присвоить им новое значение.

Как и в обычных функциях, при определении методов класса, в том числе метода __init__(), мы можем использовать аннотации типов. При этом метод __init__() всегда возвращает None, поэтому для него допускается не указывать тип возвращаемого значения. Также на классы распространяются те же рекомендации по количеству пустых строк: от другого кода класс отделяется двумя пустыми строками, а методы и логические блоки внутри класса разделяются одной пустой строкой:

class Car:
    def __init__(self, model: str, price: int | float, in_stock: bool=True):
        self.model = model
        self.price = price
        self.in_stock = in_stock

Здесь метод __init__() принимает два обязательных параметра: model и price и один необязательный параметр in_stock. Они передаются классу при создании нового объекта:

lada_granta = Car("Lada Granta Седан", 800_000)

При создании экземпляра класса Car автоматически вызывается метод __init__(), который присваивает переданные значения атрибутам созданного объекта, так как параметр self указывает на экземпляр класса, для которого применяется метод.

Методы объекта

Методы объекта – это функции, описывающие поведение объекта класса. Мы уже писали ранее функции применения скидки apply_discount() и продажи sell(), на основе которых мы можем создать методы для всех объектов класса Car.

Робот Кеша читает

В тексте не единожды уточнялось, что мы рассматриваем атрибуты и методы именно объекта или экземпляра класса, а не самого класса. Это связано с тем, что в Python существуют также атрибуты и методы класса в целом, с которыми мы познакомимся позже.

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

Тогда метод apply_discount() для применения скидки должен принимать только значение скидки percentage, так как все атрибуты объекта доступны по ссылке self, а метод sell() для продажи автомобиля изменяет атрибут in_stock объекта, поэтому ему достаточно только параметра self:

class Car:
    def __init__(self, model: str, price: int | float, in_stock: bool=True):
        self.model = model
        self.price = price
        self.in_stock = in_stock

    def apply_discount(self, percentage: int | float) -> None:
        discount_amount = self.price * (percentage / 100)
        self.price -= discount_amount
        print(f"Новая цена автомобиля {self.model}: {self.price} руб.")

    def sell(self) -> bool:
        if self.in_stock:
            print(f"Автомобиль {self.model} успешно продан")
            self.in_stock = False
            return True
        
        print(f"Автомобиль {self.model} уже был продан")
        return False

Мы дополнили класс Car как атрибутами, так и методами, и теперь он готов к работе. Больше нет необходимости работать с разрозненными словарями и функциями, а вся логика, связанная с продажей автомобилей организована в один класс.

Давайте создадим несколько объектов класса Car:

lada_granta = Car("Lada Granta Седан", 800_000)
audi_a3 = Car("Audi A3 III", 1_500_000, False)

И вызовем методы напрямую у объектов:

lada_granta.apply_discount(10)
# Вывод: Новая цена автомобиля Lada Granta Седан: 720000.0 руб.
lada_granta.sell()
# Вывод: Автомобиль Lada Granta Седан успешно продан
audi_a3.sell()
# Вывод: Автомобиль Audi A3 III уже был продан

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

Car("Renault Logan II", 380_000).sell()
# Вывод: Автомобиль Renault Logan II успешно продан

Взаимодействие объектов в ООП

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

Однако в автосалоне есть не только автомобили, но и продавцы, которые их продают. Было бы логично описать продавца как отдельную сущность, или объект, который может совершать различные действия, в том числе и продажу автомобиля. Для этого напишем класс Seller, который инициализируется атрибутом name с именем продавца, и у которого есть метод sell_car() для продажи автомобиля:

class Seller:
    def __init__(self, name: str):
        self.name = name  # Имя продавца

    def make_sell(self, car: Car) -> None:
        # Вызываем метод sell() объекта car
        if car.sell():  # Если метод вернул True, то выводим сообщение
            print(f"Продавец {self.name} продал автомобиль {car.model}")
            return True
        
        return False

Метод make_sell() в классе Seller принимает в качестве аргумента car, который является объектом класса Car, и вызывает его метод sell(). Это ключевой момент, который демонстрирует, как объекты могут взаимодействовать.

Давайте создадим новые объекты классов Car и Seller, у которого вызовем метод make_sell():

toyota_corolla = Car("Toyota Corolla XII", 2_115_000)
seller1 = Seller("Барто Агния")

# Объект seller1 взаимодействует с объектом toyota_corolla:
seller1.sell_car(toyota_corolla)
# Вывод: Автомобиль Toyota Corolla XII успешно продан
# Вывод: Продавец Барто Агния продал автомобиль Toyota Corolla XII

Теперь программа состоит из независимых, но взаимодействующих сущностей – объекта toyota_corolla класса Car и объекта seller1 класса Seller

Основные принципы ООП

Но ООП не ограничивается простым взаимодействием объектом, так как в его основе лежат три основных принципа – инкапсуляция, наследование и полиморфизм. Давайте кратком рассмотрим каждый из них, так как позже мы познакомимся с ними подробнее.

Инкапсуляция

Инкапсуляция – это принцип, который объединяет данные и методы, работающие с этими данными, в единую структуру, называемую классом. Главная идея заключается в скрытии внутренней реализации объекта от внешнего мира и предоставить к нему доступ только через строго определенный интерфейс, то есть набор методов.

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

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

Наследование – это механизм, который позволяет одному классу перенимать атрибуты и методы другого класса. Это помогает избежать дублирования кода и создает иерархию классов.

Например, если мы создаём программу для зоопарка, то можем создать базовый класс для животных Animal, который будет содержать общие для всех животных атрибуты, например, имя name или возраст age, и методы, например, кормление feed(). Затем мы можем создать класс для птиц Bird, который унаследует всё от Animal, но при этом добавит свои уникальные свойства, например, размах крыльев wingspan. Такой подход позволяет избежать дублирования кода.

Полиморфизм

Полиморфизм – это способность разных объектов использовать один и тот же метод для выполнения различных действий. То есть, один и тот же вызов метода может привести к разным результатам в зависимости от типа объекта, на котором он был вызван.

Представьте, что у нас есть классы Car для автомобиля и ElectricCar для электромобиля, и у каждого из них есть метод get_fuel_level(), чтобы узнать, сколько топлива у них осталось. Но для автомобиля метод возвращает количество бензина, а для электромобиля – процент заряда аккумулятора. Хотя метод называется одинаково, он ведёт себя по-разному в зависимости от того, на каком объекте был вызван. То есть полиморфизм позволяет нам работать с разными объектами через единый интерфейс.

Примеры

Пример 1. Проверка роли пользователя

В системе каждый пользователь описывается классом User, у которого есть атрибуты с именем username и ролью role. Для доступа к определённым ресурсам роль пользователя должна соответствовать требуемой роли, что проверяется с помощью метода has_permission():

class User:
    """Описывает пользователя системы и его роль."""
    
    def __init__(self, username: str, role: str):
        """Конструктор класса User.

        Параметры:
            username: Уникальное имя пользователя.
            role: Роль пользователя в системе.
        """
        self.username = username
        self.role = role.lower()

    def has_permission(self, required_role: str) -> bool:
        """Проверяет, соответствует ли роль пользователя требуемой.

        Параметры:
            required_role: Роль, необходимая для выполнения действия.

        Возвращает:
            True, если пользователь имеет необходимые права, иначе False.
        """
        required_role = required_role.lower()
        
        # Проверяем точное совпадение
        return self.role == required_role


# Создание объектов
user1 = User("bartenev7", "Администратор")
user2 = User("aivazovsky8", "Модератор")

# Проверка прав доступа
print(f"Пользователь {user1.username} имеет доступ к панели администрато-ра? {user1.has_permission('Администратор')}") 
print(f"Пользователь {user2.username} имеет доступ к базе данных? {user2.has_permission('Пользователь')}")

Вывод:

Пользователь bartenev7 имеет доступ к панели администратора? True
Пользователь aivazovsky8 имеет доступ к базе данных? False

Пример 2. Доставка посылки

Посылки в курьерской службе описываются классом Package, и каждую посылку можно отметить доставленной с помощью метода mark_delivered(), который изменяет статус этой посылки, то есть атрибут status:

class Package:
    """Описывает посылку и ее состояние."""
    
    def __init__(self, tracking_id: str, destination: str, status: str="В пути"):
        """Конструктор класса Package.

        Параметры:
            tracking_id: Уникальный идентификатор посылки.
            destination: Адрес доставки.
            status: Статус посылки. По умолчанию "В пути".
        """
        self.tracking_id = tracking_id
        self.destination = destination
        self.status = status

    def mark_delivered(self) -> bool:
        """Меняет статус посылки на 'Доставлено', если она еще не достав-лена.

        Возвращает:
            True, если статус успешно обновлен, False, если уже доставлена.
        """
        if self.status != "Доставлено":
            self.status = "Доставлено"
            print("Посылка доставлена")
            return True  
        print("Посылка уже была доставлена")
        return False
    

package1 = Package("2358AZ", "Тульская обл., г. Тула, ул. Ленина, д. 10")
package1.mark_delivered()
package1.mark_delivered()

Вывод:
 

Посылка доставлена
Посылка уже была доставлена

Пример 3. Управление запасами товаров в интернет-магазине

В системе учёта товаров на складе интернет-магазина каждый товар описывается классом Product и имеет атрибуты с ценой price и количеством quantity, которые должны быть неотрицательными числами. Также к цене товара можно применить скидку и изменить значение атрибута price с помощью метода apply_discount(), а проверить наличие товара позволяет метод check_stock():

class Product:
    """Описывает товар на складе магазина."""
    
    def __init__(self, name: str, price: int, quantity: int):
        """Конструктор класса Product.

        Параметры:
            name: Наименование товара. 
            price: Цена товара.
            quantity: Количество товара на складе.
        """
        # Проверка данных при создании объекта
        if price <= 0 or quantity < 0:
            raise ValueError("Цена и количество товара должны быть положи-тельными числами")
        
        self.name = name
        self.price = price
        self.quantity = quantity

    def apply_discount(self, percent: int) -> None:
        """Применяет скидку к цене товара, обновляя атрибут price.

        Параметры:
            percent: Скидка на товар в процентах.
        """
        discount_amount = self.price * (percent / 100)
        self.price -= discount_amount
        print(f"Скидка {percent}% применена. Новая цена '{self.name}': {self.price:.2f} руб.")

    def check_stock(self) -> bool:
        """Проверяет, есть ли товар в наличии.

        Возвращает:
            True, если товар есть в наличии, иначе False.
        """
        return self.quantity > 0


# Создание объектов
keyboard = Product("Клавиатура игровая мембранная проводная", 791, 50)
monitor = Product("Монитор игровой 21.5", 15000, 15)

# Взаимодействие с объектами
print(f"Наличие клавиатуры: {keyboard.check_stock()}") # True
monitor.apply_discount(15)

# Попытка создать объект с некорректными данными
try:
    bad_product = Product("Кабель USB Type-C", -150, 5)
except ValueError as e:
    print(f"Ошибка при создании товара: {e}")

Вывод:

Наличие клавиатуры: True
Скидка 15% применена. Новая цена 'Монитор игровой 21.5': 12750.00 руб.
Ошибка: Цена и количество товара должны быть положительными числами

Итоги

  • Процедурное программирование – это парадигма программирования, основанная на использовании функций для организации кода.
  • Объектно-ориентированное программирование (ООП) – это парадигма программирования, основанная на использовании объектов для организации кода.
  • Объект – это сущность, объединяющая данные и функции.
  • Класс – это шаблон, определяющий свойства и поведение объекта.
  • Атрибуты объекта – это переменные, предназначенные для хранения данных о каждом конкретном объекте класса.
  • Магические методы – это методы, позволяющие настраивать встроенное поведение объектов класса.
  • Магический метод __init__() называется конструктором класса и позволяет задать начальные значения атрибутов нового объекта.
  • Методы объекта – это функции, описывающие поведение объекта класса.
  • Первым параметром метода объекта должна быть ссылка на конкретный объект, для которого вызывается метод. Такой параметр принято называть self.
  • Параметр self позволяет обращаться ко всем атрибутам и методам объекта,
  • В основе ООП лежат три основных принципа – инкапсуляция, наследование и полиморфизм.
  • Инкапсуляция – это принцип, который объединяет данные и методы, работающие с этими данными, в единую структуру, называемую классом.
  • Наследование – это механизм, который позволяет одному классу перенимать атрибуты и методы другого класса.
  • Полиморфизм – это способность разных объектов использовать один и тот же метод для выполнения различных действий.

Задания для самопроверки

1. Чем ООП отличается от процедурного программирования?

Процедурное программирование организует код в виде функций, а ООП в виде классов и объектов.

2. Объясните разницу между классом и объектом (экземпляром класса) на примере из реальной жизни.

Класс – это шаблон, определяющий свойства и поведения объекта, а объект – это сущность, объединяющая данные и функции. Например, есть класс кактусов, который задаёт свойства каждого кактуса: цвет, колючки, размер, и его поведение: сохнуть без воды. А также есть конкретный и любимый кактус на подоконнике, как объект класса кактусов, который почти без колючек и не сохнет без воды месяц.

3. Создайте класс для точки Point, который инициализируется атрибутами x и y с соответствующими координатами. Создайте объект этого класса с атрибутами x = 5 и y = 10. Выведите содержимое словаря __dict__ для этого объекта.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(5, 10)
print(point.__dict__)
# Вывод: {'x': 5, 'y': 10}

В словаре мы видим атрибуты экземпляра x и y вместе с их значениями.

4. Создайте класс для книги Book, который инициализируется атрибутами title с названием книги и author с именем автора. Создайте два разных объекта этого класса и выведите на экран значение title для каждого из них.

class Book:
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author


book1 = Book("Война и мир", "Лев Николаевич Толстой")
print(book1.title)
# Вывод: Война и мир
book2 = Book("451 градус по Фаренгейту", "Рэй Брэдбери")
print(book2.title)
# Вывод: 451 градус по Фаренгейту

 

5. Создайте класс для лампы Lamp, который инициализируется атрибутом is_on со значением по умолчанию False. Он характеризирует состояние лампы: включена (True) или выключена (False). Создайте в этом классе метод toggle_switch(), который меняет значение атрибута is_on на противоположное (True на False и наоборот). Создайте объект этого класса и вызовите метод дважды, выводя состояние is_on после каждого вызова.

class Lamp:
    def __init__(self, is_on=False):
        self.is_on = is_on

    def toggle_switch(self) -> None:
        self.is_on = not self.is_on


my_lamp = Lamp()
my_lamp.toggle_switch()
print(f"Включаем: {my_lamp.is_on}") 
# Вывод: Включаем: True
my_lamp.toggle_switch()
print(f"Выключаем: {my_lamp.is_on}") 
# Вывод: Выключаем: False