ООП на Python

Гайд от Матвея 🥸

Сайт

https://ra1nbow.xyz
[!info] Github

https://github.com/ra1nbow1
Посвящается всем, кто прочитал 2 предыдущих раздела про Пайтон и хочет расширять свои знания. Приступаем к объектно-ориентированному программированию.

Это кто? who?

Так вот, ООП - это как методология программирования, но не такая скучная. Она основана на идее "объектов", которые ведут себя как небольшие, самодостаточные части кода. Каждый объект содержит в себе какие-то данные и функции, которые с ними работают.
Представьте, что ваша программа - это фабрика, а классы - это инструменты в этой фабрике. У каждого инструмента есть свои функции и свои характеристики, но все они работают вместе, чтобы создать конечный продукт - ваше приложение.
Основные плюсы ООП:

  1. Модульность и повторное использование кода - проще делать сложное: вы можете разбить свою программу на маленькие, управляемые кусочки, что делает ее проще для понимания и разработки. Выживаете от дублирования кода и можете использовать классы снова и снова.
  2. Инкапсуляция - прятать сложность: вы можете скрыть внутренние детали своих объектов, предоставив только то, что нужно для работы. Таким образом, вы можете сделать свой код более безопасным и чистым.
  3. Наследование - создавать новое из старого: наследование позволяет вам создавать новые классы на основе существующих. Это как строить новый автомобиль на основе старой модели, но с новыми функциями.
  4. Полиморфизм - сделать ваш код гибким: полиморфизм позволяет вам использовать объекты разных классов так, как будто они одного класса. Это удобно, если вам нужно использовать разные объекты в одном контексте.
  5. Расширяемость и поддерживаемость: если вам нужно что-то поменять в вашей программе, ООП позволит вам сделать это быстро и безболезненно. Вы сможете изменить один класс, и это не повлияет на другие части вашей программы.
    Так что в итоге, ООП помогает делать ваш код более читаемым, понятным и гибким.

Классы

В объектно-ориентированном программировании, классы и объекты являются основными концепциями. Классы представляют собой шаблоны для создания объектов, а объекты - конкретные экземпляры этих классов. Давайте рассмотрим эти концепции более подробно и приведем примеры.
Класс - это тип данных, описывающий набор атрибутов (переменных) и методов (функций), которые могут иметь объекты этого класса.

Определение класса

class MyClass:
    # Атрибуты класса
    class_variable = "Я - атрибут"
    # Метод класса
    def class_method(self):
        return "Я - метод"

Объекты

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

Создание объекта

# Создание объекта класса MyClass
my_object = MyClass()

Доступ к атрибутам и вызов методов объекта:

# Доступ к атрибутам объекта
print(my_object.class_variable)
# Вызов метода объекта
print(my_object.class_method())

Давайте рассмотрим пример класса **Person**, который представляет собой человека с атрибутами **name** и **age** и методом **say_hello**:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def say_hello(self):
        return f"Привет, я {self.name}, мне {self.age} лет."
# Создание объекта класса Person
person1 = Person("Матвей", 19)
# Доступ к атрибутам объекта
print(person1.name)  # Matvey
print(person1.age)   # 19
# Вызов метода объекта
print(person1.say_hello())  # Привет, я Матвей, мне 19 лет.

Атрибуты

Теперь немного подробнее. Атрибуты - это переменные, связанные с определенным объектом. Они представляют состояние объекта и могут хранить данные, которые описывают его характеристики или свойства. Атрибуты могут быть как публичными, так и приватными.

Публичные атрибуты

Эти атрибуты доступны извне объекта и могут быть прочитаны и изменены непосредственно. Названия начинаются просто с буквы.

class Person:
    def __init__(self, name, age):
        self.name = name  # Публичный атрибут
        self.age = age    # Публичный атрибут
person1 = Person("Матвей", 19)
print(person1.name)  # Матвей

Приватные атрибуты

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

class Person:
    def __init__(self, name, age):
        self.__name = name  # Приватный атрибут
        self.__age = age    # Приватный атрибут
person1 = Person("Матвей", 19)
# print(person1.__name)  # AttributeError: 'Person' object has no attribute '__name'

Методы

Публичные методы

Эти методы доступны извне объекта и могут быть вызваны непосредственно. Названия также начинаются просто с букв.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def get_name(self):  # Публичный метод
        return self.name
person1 = Person("Матвей", 19)
print(person1.get_name())  # Матвей

Приватные методы

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

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __display_info(self):  # Приватный метод
        return f"Имя: {self.name}, Возраст: {self.age}"
person1 = Person("Matvey", 19)
# print(person1.__display_info())  # AttributeError: 'Person' object has no attribute '__display_info'

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

Конструкторы

Их иногда называют “инициализаторы”.

class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
car1 = Car("BMW", "3-series")
print(car1.brand)  # BMW
print(car1.model)  # 3-series

Деструкторы

class MyClass:
    def __del__(self):
        print("Класс удален")
obj = MyClass()
del obj  # Класс удален

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

Инкапсуляция - это концепция объектно-ориентированного программирования, которая позволяет скрыть внутреннюю реализацию объекта от внешнего мира и обеспечивает доступ к объекту только через определенные методы.
Рассмотрим класс **Person**, где мы будем использовать инкапсуляцию для скрытия атрибута **__age**:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age  # Приватный атрибут
    def get_age(self):  # Публичный метод для доступа к приватному атрибуту
        return self.__age
    def set_age(self, age):  # Публичный метод для изменения приватного атрибута
        if age > 0:
            self.__age = age
# Создание объекта класса Person
person1 = Person("Матвей", 19)
# Попытка доступа к приватному атрибуту напрямую вызовет ошибку
# print(person1.__age)  # AttributeError: 'Person' object has no attribute '__age'
# Доступ к приватному атрибуту через публичный метод
print(person1.get_age())  # Вывод: 19
# Изменение приватного атрибута через публичный метод
person1.set_age(20)
print(person1.get_age())  # Вывод: 20

В этом примере атрибут **__age** мы объявили как приватный. Это означает, что он доступен только внутри класса. Мы предоставляем публичные методы **get_age()** и **set_age()**, чтобы получать и изменять значение атрибута **__age**. Таким образом, мы скрываем внутреннюю реализацию и защищаем атрибут от прямого доступа извне.
Почему полезно использовать инкапсуляцию - чтобы наши данные не могли быть случайно испорчены или изменены без нашего разрешения. Только наши методы, “знающие” все тонкости работы объекта, имеют к ним доступ. Также это позволяет нам удобнее управлять данными и скрывает сложности реализации от пользователей класса. Если внутренности объекта меняются, но интерфейс остается тем же, внешний мир даже не заметит изменений, как будто мы сделали ремонт в квартире, но соседи и дальше считают, что мы живем как раньше.

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

Давайте представим себе семейную историю. Есть родительские и дочерние классы.
Представьте, что у нас есть класс "Животное" (родительский класс), который определяет основные характеристики животного, например, его имя. Теперь у нас есть два дочерних класса: "Собака" и "Кошка". Эти классы наследуют все атрибуты и методы класса "Животное", такие как возможность называть животное по имени.
Каждый из дочерних классов может также иметь свои уникальные характеристики. Например, у собаки есть специальный метод, который позволяет ей лаять, а у кошки - метод, позволяющий мяукать.
Таким образом, наследование позволяет нам использовать функциональность родительского класса в дочерних классах, а также добавлять или изменять функциональность, специфичную для дочерних классов.
Смотрим пример:

class Animal:
    def __init__(self, name):
        self.name = name
    def make_sound(self):
        pass
class Dog(Animal):
    def make_sound(self):
        return f"{self.name} говорит: Гав!"
class Cat(Animal):
    def make_sound(self):
        return f"{self.name} говорит: Мяу!"
dog = Dog("Пэс")
print(dog.make_sound())  # Пэс говорит: Гав!
cat = Cat("Кiшка")
print(cat.make_sound())  # Кiшка говорит: Мяу!

Здесь класс Animal - это наш "родительский" класс, а классы Dog и Cat - "дочерние". Оба дочерних класса наследуют атрибут name и метод make_sound(), но переопределяют метод, чтобы предоставить специфичное для них поведение.
Зачем оно надо? Представим, что мы пишем программу, и у нас есть класс, который уже делает много крутых штук. Но потом нам нужно создать новый класс, который делает что-то очень похожее, но с некоторыми дополнениями или изменениями. Неужели нужно писать все заново? Вот именно! Наследование приходит на помощь!
Просто создадим новый класс, который "наследует" все функции и свойства от своего первого класса. Берем уже собранный кусочек и добавляем к нему новые детали, чтобы создать что-то новенькое!
И главное, когда мы наследуем класс, это не только удобно, но и круто, потому что код становится более организованным и проще для понимания. Так что наследование помогает нам создавать классы, как реальные профессионалы!

Полиморфизм

Это просто великолепие! Данная штука позволяет объектам разных классов вести себя по-разному, но при этом использовать один и тот же метод. Вот как это работает:
Есть несколько классов, каждый из которых имеет метод с одним и тем же названием. Но в каждом из этих классов метод делает что-то уникальное, подходящее для этого класса.
Когда мы вызываем метод у объекта, Python знает, к какому классу принадлежит этот объект, и вызывает соответствующий метод для этого класса.
Вот пример. У нас есть классы Dog и Cat, и у каждого из них есть метод make_sound():

class Dog:
    def make_sound(self):
        print("Гав!")
class Cat:
    def make_sound(self):
        print("Мяу!")

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

dog = Dog()
cat = Cat()
dog.make_sound()  # Гав!
cat.make_sound()  # Мяу!

Классы Dog и Cat оба имеют make_sound(), но каждый из них делает разные звуки. Это и есть полиморфизм. Мы используем один и тот же метод, но он ведет себя по-разному в зависимости от класса объекта.
Используя полиморфизм, мы:

  1. Уменьшаем дублирование кода: если у нас есть несколько классов с похожими методами, мы можем использовать полиморфизм, чтобы написать один метод, который будет работать для всех этих классов. Это уменьшает количество повторяющегося кода в нашей программе.
  2. Облегчаем добавление новых функций: когда у нас появляется новый класс, который должен вести себя так же, как уже существующие классы, мы можем просто наследовать его от нужного класса и переопределить соответствующие методы. Таким образом, мы можем добавить новый функционал, не меняя существующий код.
  3. Улучшаем читаемость кода: используя полиморфизм, мы можем сделать наш код более понятным и легким для понимания. Методы, которые ведут себя по-разному в разных классах, могут иметь одинаковые имена и параметры, что делает код более предсказуемым.

Перегрузка операторов

Перегрузка операторов в Python - это как волшебство, которое позволяет вам изменять поведение стандартных математических, логических и других операторов, чтобы они работали особым образом для ваших собственных классов. Представьте, что вы можете настроить свой собственный мир правил, в котором **+** может означать не только сложение чисел, но и объединение строк или слияние списков - все зависит от того, как вы этот оператор перегрузите. Я в шоке просто.
Например, давайте представим, что у нас есть класс **Cat**, и мы хотим, чтобы оператор **+** добавлял двух котов вместе, а не складывал их весы. Вот как это выглядит на практике:
Класс представляет кота и имеет два атрибута: **name** и **color**.

class Cat:
    def __init__(self, name, color):
        self.name = name
        self.color = color

Мы хотим, чтобы оператор **+** добавлял двух котов вместе. Для этого мы перегружаем оператор **+** в нашем классе **Cat** с помощью метода **__add__**. Дописываем класс:

class Cat:
    def __init__(self, name, color):
        self.name = name
        self.color = color

		def __add__(self, other_cat):
		    return Cat(self.name + " и " + other_cat.name, "смешанный")

Этот метод принимает два аргумента: **self** (ссылка на текущий объект) и **other_cat** (другой кот, с которым мы хотим сложить текущего кота). Затем он создает новый объект класса **Cat**, объединяя имена двух котов в одну строку и устанавливая цвет нового кота как "mixed".
Теперь, когда мы выполняем операцию **cat1 + cat2**, интерпретатор Python вызывает метод **__add__** объекта **cat1** и передает **cat2** в качестве аргумента **other_cat**. В результате создается новый объект **combined_cat**, который представляет двух объединенных котов.

cat1 = Cat("Кiшка1", "серый")
cat2 = Cat("Кiшка2", "белый")
combined_cat = cat1 + cat2
print(combined_cat.name)  # Кiшка1 и Кiшка2
print(combined_cat.color)  # смешанный

Ну что уж тут говорить, перегрузка операторов добавляет просто безграничные возможности, предел которых определяется только фантазией разработчика.
Ну че? Дальше будем познавать принципы SOLID? Когда-нибудь потом 😁