ООП. Введение в объектно-ориентированное программирование в Python

Знание принципов объектно-ориентированного программирования позволяет сделать код более гибким и читаемым, а также избегать повтора. С помощью классов можно создавать даже собственные типы данных. Давайте начнем с основного синтаксиса.

Базовый синтаксис

Самый простой класс можно создать с помощью ключевого слова class:

class MyClass:
    pass

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

Методы

Метод - это обычная функция, которая находится внутри класса. У метода всегда первым аргументом является self - ==экземпляр класса==. Экземпляр класса - это конкретное воплощение класса, которое имеет свои уникальные значения для атрибутов данных и методов.

Существуют особые ==магические методы== (их еще называют dunder methods, сокр. от double underscores), которые позволяют влиять на особый функционал класса. Все они имеют двойные нижние подчеркивания по бокам.

Одним из таких является метод __init__. Он влияет на то, что будет происходить при создании экземпляра класса. Давайте создадим его внутри нашего класса.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Давайте теперь создадим экземпляр класса Person:

walter = Person("Walter", 52)

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

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def hello(self):
        print("Hello")

Теперь мы берем наш экземпляр walter и вызываем метод hello:

walter.hello()

Все работает, сообщение Helloнапечаталось

Атрибуты

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

walter.age #52
walter.name #Walter

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

class Person:
    type = "human"

Теперь у нас есть атрибут type. Его можно было объявить и в методе __init__:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.type = "human"

Еще можно добавлять атрибуты прямо экземплярам класса:

walter.surename = "White"

С двумя подчеркиваниями могут быть еще и атрибуты, например, self.__class__.__name__ является атрибутом, который хранит имя класса.

Изменение атрибутов

Атрибутам присваивается новое значение с помощью оператора присваивания =:

walter.age = 50

Теперь атрибуту age присвоено значение 50. Таким же образом можно менять и все остальные атрибуты.

Удаление атрибутов

Атрибуты удаляются с помощью ключевого слова del:

del walter.age

Если попытаться обратиться к этому атрибуту, то будет получена ошибка:

Traceback (most recent call last):
  File "D:\...\main.py", line 9, in <module>
    print(walter.age)
          ^^^^^^^^^^
AttributeError: 'Person' object has no attribute 'age'

Сообщение говорит, что такого атрибута нет, а значит удаление прошло удачно.

Обращение к атрибутам внутри методов

Обращение к атрибутам всегда происходит через self, Например, чтобы обратиться к атрибуту age внутри какого-либо метода в классе, нужно написать self.age. Давайте создадим метод say_hello и заодно научимся вызывать методы.

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_hello(self):
        print(f"Hello, i'm {self.name} and i'm {self.age} yo")

walter = Person("Walter", 52)
walter.say_hello()

Программа работает как ожидалось:

Hello, i'm Walter and i'm 52 yo

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

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

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_hello(self):
        print(f"Hello, i'm {self.name} and i'm {self.age} yo")


class ChildOfPerson(Person):
    def __init__(self, name, age, toys):
        super().__init__(name, age)
        self.toys = toys

Теперь создадим экземпляр класса ChildofPerson:

james = ChildOfPerson('James', 8, ["lego", "ps5"])

И проверим все ли на месте и ко всему ли мы имеем доступ:

print(james.name)
print(james.age)
print(james.toys)

james.say_hello()

И имеем такой вывод:

James
8
['lego', 'ps5']
Hello, i'm James and i'm 8 yo

Все работает, но как? Давайте вернемся к коду выше. Метод __init__() класса ChildOfPerson устроен так:

    def __init__(self, name, age, toys):

Передаем все те же аргументы, что и в __init__ класса Person, но дополнительно добавляем аргумент toys.

super().__init__(name, age)

Здесь мы вызываем метод __init__ родительского класса и передаем в него аргументы. Мы должны обращаться к родительскому классу как super().

self.toys = toys

Тут мы просто прописываем еще один атрибут для класса.

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

Множественное наследование

Множественное наследование - подход, при котором у класса есть несколько родительских классов. Например, базовый пример выглядит вот так:

class Biological:
    def live(self):
        print('Breathe, eat, sleep')

class Social:
    def communicate(self):
        print('I can speak')

class Human(Biological, Social):
    def __init__(self, name) -> None:
        self.name = name

bob = Human('Bob')

bob.live()
bob.communicate()

Как и ожидалось, вывод такой:

Breathe, eat, sleep
I can speak

Атрибут __mro__

Существует такой атрибут - __mro__. Он определяет порядок разрешения методов. Например, для класса Human этот атрибут будет таким:

print(Human.__mro__) 
(<class '__main__.Human'>, <class '__main__.Biological'>, <class '__main__.Social'>, <class 'object'>)

Как мы видим, все понятно, ничего сложного. Если мы обращаемся к какому-то методу класса Human, то он сначала ищется в самом классе, а затем в Biological а затем в Social. Этот пример довольно простой и поэтому не должно возникать никаких сложностей. Но бывает так, что возникает конфликт имен, об этом ниже.

Проблема ромбовидного наследования

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

class Root:
    def ping(self):
        print(f'{self}.ping() in Root')

    def pong(self):
        print(f'{self}.pong() in Root')

    def __repr__(self):
        cls_name = type(self).__name__
        return f'<instance of {cls_name}>'


class A(Root):
    def ping(self):
        print(f'{self}.ping() in A')
        super().ping()

    def pong(self):
        print(f'{self}.pong() in A')
        super().pong()


class B(Root):
    def ping(self):
        print(f'{self}.ping() in B')
        super().ping()

    def pong(self):
        print(f'{self}.pong() in B')


class Leaf(A, B):
    def ping(self):
        print(f'{self}.ping() in Leaf')
        super().ping()


leaf1 = Leaf()
leaf1.ping()
print('*' * 40)
leaf1.pong()

Магический метод __repr__ нужен для строкового представления, когда мы передаем экземпляр класса в print(). Есть другой метод - __str__, но там есть некоторые отличия.

При запуске этого кода получается такой вывод.

<instance of Leaf>.ping() in Leaf
<instance of Leaf>.ping() in A
<instance of Leaf>.ping() in B
<instance of Leaf>.ping() in Root
****************************************
<instance of Leaf>.pong() in A
<instance of Leaf>.pong() in B

В этом примере класс Leaf наследуется от классов A и B, которые оба наследуются от класса Root. Когда мы вызываем leaf1.ping() или leaf1.pong(), возникает вопрос: какой метод будет вызван — метод из A или метод из B.

Python решает проблему ромбовидного наследования с помощью Метода разрешения порядка (MRO), который я вам показывал ранее. MRO определяет порядок, в котором Python ищет методы и атрибуты в иерархии классов. В Python используется алгоритм C3 Linearization, который обеспечивает последовательный и предсказуемый порядок поиска.

Это еще не все

ООП - обширная тема, которая требует длительного изучения. На этом сайте будут выпускаться еще статьи, которые позволят разобраться в этом. Сохраните этот сайт в закладки и делитесь им с друзьями.


Модули в языке Python. Разбиение программ на модули. Установка сторонних библиотек

Для написания сложных программ необходимо использовать сторонние библиотеки. Они позволяют использовать готовый функционал. Также можно использовать модули для компоновки вашего приложения.

Pattern matching в Python - конструкция match case

В Python 3.10 был добавлен новый функционал - сопоставление с шаблонами с помощью ключевых слов match и case. В статье также рассматривается производительность match/case по сравнению с if/else.

Классы исключений. Создание собственных исключений

В этой статье описано, что означают различные классы исключений. Также рассмотрено создание собственных классов исключений.

Обработка исключений. Конструкция try/except/finally

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