В программировании есть несколько основных парадигм — подходов, которые определяют, как программисты структурируют и организуют свой код. Например, процедурное программирование — самый простой и прямолинейный подход. Программы организованы как цепочки команд или процедур, выполняемых последовательно. Например, кулинарный рецепт очень похож на программу, написанную в парадигме процедурного программирования: ведь рецепт — это, по сути, набор инструкций, которые нужно выполнить строго по порядку, чтобы получить готовое блюдо. Есть функциональное программирование. Этот подход основан на концепции математических функций, которые принимают входные данные и возвращают результат. Допустим, вы написали письмо и хотите его перевести на другой язык, проверить на грамматику и орфографию, и затем распечатать. В функциональном программировании каждый из этих шагов может быть функцией, причем каждая функция всегда возвращает один и тот же результат при одних и тех же входных данных. Одним из наиболее популярных подходов сейчас является объектно-ориентированное программирование, ООП, где код организуется вокруг объектов и классов. Объектно-ориентированный подход поддерживается многими языками программирования и начинающим разработчикам стоит детальнее разобраться с тем, что же на практике означают страшные слова «полиморфизм», «инкапсуляция» и другие термины, применяемые в этой парадигме. В этой статье я расскажу о ключевых понятиях, связанных с ООП, и на примере Python-кода разберу, как одна и та же задача может быть успешно решена с использованием процедурного и объектного подхода. Но обо всём по порядку; начнём, традиционно, с основ, а поможет мне в этом выдуманный хоккеист Степан. Вечеринка по случаю победы хоккейной команды плавно переходила в ту стадию, когда все уже чувствовали себя самыми умными и красивыми, но ещё стояли на ногах. Однако Степан Осечкин ощущал себя чужим на этом празднике жизни. Хоккеист Осечкин был не просто хоккеистом: всё свободное время он проводил за изучением новых навыков. Очередным его увлечением стало программирование. И сейчас он раз за разом прокручивал в голове слова, которые прочёл в статье про ООП. Слова были понятными, но уловить общий смысл никак не получалось: Основная идея ООП заключается в том, чтобы представить программу в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определённого класса. Что за класс? Чем он отличается от экземпляра? А как они взаимодействуют? Вот в хоккее всё намного проще: есть хоккеисты, у каждого есть клюшка, позиция на поле и номер на спине; но один двигается быстрее, а другой бъёт сильнее… И тут ему пришла в голову мысль, которая заставила улыбнуться. «Хоккей и программирование похожи гораздо больше, чем я думал!», — сказал он про себя. — Класс в ООП — это как понятие «хоккеист» в реальном мире, — продолжал размышлять Степан, — Это обобщённое понятие, определяющее набор свойств хоккеиста: имя, номер на свитере, позиция. А ещё действия, которые может совершать этот «обобщённый хоккеист»: он должен бросать шайбу, отбивать её, ездить на коньках… Например, на команду тренера «Бей!» любой хоккеист должен среагировать — ударить клюшкой… по шайбе, как правило. — А вот «объект класса» — это конкретный хоккеист, например, я, Степан Осечкин. Я - экземпляр класса «хоккеист», у меня есть все свойства, которые определены в классе, и я могу выполнять все те действия, что описаны для хоккеиста. И вот по команде «Бей!» любой экземпляр-хоккеист ударит по шайбе; один хоккеист ударит сильнее, другой — точнее, но обязательно ударит. — Прикольно, — подумал Степан, — в хоккее, как и в программировании, мы все играем по определенным правилам и используем общие шаблоны, но каждый из нас уникален и может привнести что-то своё в игру. Класс в программировании — это шаблон или чертёж для создания объектов (экземпляров класса). Класс описывает свойства (атрибуты) и действия (методы), которыми могут обладать все объекты этого класса. Для описания класса в Python используется: ключевое слово class, имя класса, двоеточие, и тело класса. Простейший класс в Python может выглядеть так: Согласно PEP8 названия классов дают в единственном числе и пишут с большой буквы. В теле класса обычно объявляют атрибуты, конструктор __init__() и другие методы. Конструктор класса __init__() — это специальный метод, который автоматически вызывается при создании нового объекта класса. Например, хоккейная команда может обзавестись новым игроком, которому сразу нужно выдать форму с номером, определить его позицию на поле, внести имя в список игроков и так далее. Конструктор класса передаёт в новый объект все необходимые свойства. Для примера опишем класс HockeyPlayer, в котором объявим конструктор класса: Конструктор __init__() устанавливает начальные значения атрибутов для каждого объекта класса HockeyPlayer. Каждый новый объект-хоккеист будет обладать именем self.name, фамилией self.surname, а также возрастом и принадлежностью к команде. Это атрибуты экземпляра класса — их указывают при создании экземпляра класса, например вот так: Будет здорово, если любой хоккеист (экземпляр класса HockeyPlayer) сможет сообщить всем интересующимся своё полное имя. Это уже действие, а значит понадобится метод экземпляра; объявим его в классе: Метод get_full_name — это метод экземпляра, он возвращает полное имя хоккеиста, объединяя имя и фамилию (атрибуты экземпляра) в одну строку. Теперь можно создавать сколько угодно хоккеистов (хоть целую команду) и у каждого можно посмотреть атрибуты или спросить полное имя: Уже неплохо, но непонятно какая роль на поле у каждого игрока. Где-то нужно указать  позиции игроков! Позиция на поле есть у каждого игрока. И обычно на хоккейный матч команда заявляет около 20 хоккеистов — например, 12 нападающих, 6 защитников и 2 вратаря. В странной команде Степана собрались одни нападающие, других в эту команду не берут. А это значит, что каждому игроку можно присвоить новый атрибут default_position, и в качестве значения сразу же указать Forward. Раз этот атрибут будет одинаковым для всех участников команды, то и присвоить его можно не экземплярам, а всему классу целиком. Для этого предусмотрены атрибуты класса. default_position — это атрибут класса, который будет общим для всех экземпляров этого класса (если его не переопределить). Получить его значение можно через любой экземпляр класса, например вот так: Но можно получить атрибут класса напрямую, не создавая экземпляр: — …если существуют атрибуты класса, то должны существовать и методы класса, — задумчиво подытожил Степан. Метод — это функция, находящаяся внутри класса. В классе HockeyPlayer уже есть один метод — это метод экземпляра get_full_name, он возвращает полное имя хоккеиста. Первым аргументом метода экземпляра указывают объект класса (его принято называть self); этот аргумент указывает на сам экземпляр класса. Через параметр self можно менять состояние объекта и обращаться к другим его методам и атрибутам. Например, при формировании полного имени игрока обращение к атрибутам экземпляра происходит не по имени атрибута, а через self: — f'{self.name} {self.surname}'. Экземпляр возвращает свой атрибут name и свой атрибут surname. По умолчанию все методы в классе привязаны к экземпляру класса, а не к самому классу. Однако с помощью декоратора @classmethod можно объявить метод, который привязан к самому классу, а не к его экземпляру. Такие методы называют методами класса. Примером такого метода служит get_default_position, который возвращает значение атрибута класса default_position. При объявлении методов класса первым параметром указан cls. Этот параметр указывает на сам класс, а не на объект этого класса. Название cls — традиционно принятое: можно назвать его иначе, но лучше соблюдать традиции, так всем будет проще читать ваш код. Помимо методов класса и экземпляра можно объявлять статические методы. Они описываются при помощи декоратора @staticmethod. Статическим методам не нужен параметр, указывающий на объект или класс (ни self, ни cls). Их можно воспринимать как методы, которые «не знают, к какому классу относятся». Статические методы в основном используются как вспомогательные функции и работают с данными, которые им переданы. Объявим в классе статический метод get_greeting, который будет просто выводить фразу Привет, хоккеист!: Вызвать статический метод можно обращаясь к нему через имя класса или через экземпляр класса. Обозначения self и cls были выбраны в результате соглашения об именовании параметров. Это не зарезервированные ключевые слова, вместо них могут быть использованы любые иные. Но в PEP8 для обозначения экземпляра класса рекомендовано использовать имя self, а для обозначения параметра, который принимает ссылку на класс — имя cls. Обычно разработчики следуют этой рекомендации — так код будет более понятным (а код, как известно, чаще читают, чем пишут). На следующей тренировке Степан чувствовал себя частью программы: тренер вызывал методы бей!, давай_рывок! и куда_прёшь?! для разных хоккеистов, и эти методы срабатывали для любого экземпляра-хоккеиста в команде. Но сосредоточиться мешали три страшных слова из теории ООП, да и запомнить их с первой попытки не удалось: что-то типа морфи… капсу… и какое-то следование! В ходе атаки на ворота Степан врезался в бортик с такой силой, что немедленно вспомнил все забытые термины, в том числе — «наследование», «полиморфизм» и «инкапсуляция». «Всё нормально, сотрясения нет», сказал врач. Когда Степан поднялся на ноги — всё встало на свои места. Наследование — свойство системы, позволяющее на основе существующего класса создать новый класс, и при этом частично или полностью заимствовать в наследнике функциональность или атрибуты родителя. Например, можно описать базовый класс HockeyPlayer, а затем создать дочерний класс GoalKeeper, который наследует атрибуты и методы класса HockeyPlayer, но в который будут добавлены специфические для вратаря свойства и методы — например, метод not_my_fault(). Полиморфизм — возможность единообразно обрабатывать объекты с различной реализацией при условии наличия общего интерфейса. Любому хоккеисту важно поддерживать хорошую физическую форму, независимо от того, какую роль он играет на льду: вратарь он, защитник или нападающий. Все они — объекты класса Хоккеист, и к ним можно применить один и тот же метод — тренироваться(). Для разных игроков метод тренироваться() будет выглядеть по-разному: вратарь будет больше времени уделять отражению бросков и тренировать реакцию, защитник будет накачивать физическую силу и отрабатывать оборону, а нападающий посвятит тренировку скорости и точности бросков. Тем не менее, для любого экземпляра класса Хоккеист и для экземпляров классов-наследников (для классов Вратарь, Защитник или Нападающий) тренер может вызвать метод тренироваться() — и этот метод сработает. Такой подход в ООП и называется полиморфизмом: для объектов базового класса и для объектов унаследованных классов можно вызывать одни и те же методы; методы сработают, но могут вести себя по-разному, в зависимости от объекта. Инкапсуляция — свойство системы, позволяющее объединить в объекте и данные, и методы для работы с ними. Данные объекта скрыты от остальной программы. Хоккейный болельщик не должен знать все детали тренировочного процесса: ему достаточно быть в курсе основных правил игры и наблюдать за матчем. Все детали подготовки, особенности диеты спортсменов, ход тренировок и разработка игровой тактики скрыты от наблюдателя и проявляются лишь по внешним результатам работы — по умению игроков контролировать шайбу, хорошо передвигаться по льду и забивать голы. Это и есть пример инкапсуляции в контексте хоккея. Степан — открытый и любознательный спортсмен. Он понимает, что высококлассными хоккеистами становятся лишь те, кто не только нарабатывает технические навыки на льду, но и думает стратегически, анализирует игру. Откровение, касающееся общности программирования и хоккея, очень его вдохновило, и он начал задумываться, как бы использовать свои навыки программирования для улучшения игры. — Хороший хоккеист — он стратег, а стратег осознает геометрию игрового поля и использует её для своего преимущества, — размышлял Степан, — Надо бы написать программу для этого. Он решил начать с простого — создать программу для вычисления площади и периметра игрового поля. На вычисленном «программном поле» можно будет разрабатывать стратегии, которые помогут использовать пространство более эффективно. Степан поторопился и написал решение без ООП, через функции: Решение оказалось достаточно простым — две функции, которые принимают длину и ширину и возвращают нужный результат. А можно ли вместо функции написать класс? Безусловно! В самом простом виде решение будет примерно таким: Оба подхода верны и работоспособны. Но тут Степан вспомнил, что продвинутые хоккейные тренеры теперь применяют круглые тренировочные поля. Получается, программу надо доработать: добавить расчёт периметра и площади для круга. При работе с функциями пришлось создать две дополнительные функции для новой фигуры: А если какой-нибудь тренер придумает треугольное или семиугольное поле — придётся плодить новые функции для каждой новой фигуры. И как потом разбираться во всём этом многообразии? И тут Степана как клюшкой ударило: — У меня же есть ООП! Эту задачу надо решать при помощи объектного подхода — не просто написать класс взамен функции, а применить ключевые преимущества этой парадигмы. В этом примере Shape — базовый класс, от которого наследуются классы для конкретных фигур. Сейчас их всего две, но добавить новую фигуру теперь не составит большого труда. Класс для каждой новой фигуры унаследует всё то, что есть в базовом классе, а при необходимости базовые возможности можно расширить или изменить. Метод calculate доступен всем наследникам; он принимает тип вычисления (area или perimeter) и вызывает соответствующий метод. Если вдруг появятся трёхмерные хоккейные поля — в базовый класс можно добавить и тип volume (объём) — и этот тип вычислений будет доступен во всех классах-наследниках. Полиморфизм позволил использовать одну функцию для вызова различных методов в зависимости от переданного аргумента. В Python метод или атрибут, имя которого начинается с подчёркивания, является защищённым. Использование подчеркивания — это скорее норма поведения, подразумевающая, что другие программисты не должны напрямую обращаться к этим атрибутам или методам. Технически доступ к этим атрибутам и методам всё равно остаётся открыт, но любой программист поймёт: не надо их трогать. Атрибуты длины, ширины и радиуса как раз такие (_length, _width, _radius), их можно получить или изменить только через методы класса. Таким образом, в представленном решении Степан реализовал все ключевые принципы объектно-ориентированного подхода: Переиспользование кода (наследование). В ООП можно создать общий базовый класс (в нашем случае это класс Shape), который содержит общие методы и атрибуты для всех производных классов. Это снижает дублирование кода и упрощает его поддержку и расширение. Инкапсуляция. ООП позволяет скрывать внутренние детали класса и предоставлять простой интерфейс для взаимодействия с ним. В нашем примере, все атрибуты (длина, ширина, радиус) скрыты внутри классов и доступ к ним предоставляется только через методы класса. Полиморфизм. ООП позволяет использовать один и тот же интерфейс для различных типов объектов. В нашем примере метод calculate можно вызывать для вычисления площади и периметра разных типов фигур. Преимущества использования ООП становятся особенно заметными при работе с большими программами, где есть множество взаимосвязанных объектов. Благодаря ООП, код становится модульным, легко расширяемым и поддерживаемым. С другой стороны, для небольших задач и скриптов использование процедурного программирования может быть более простым и быстрым. После очередного матча тренер сообщил Степану, что на следующем чемпионате будет лотерея: если возраст хоккеиста равен суммарному числу букв в его имени и фамилии — ему выдадут приз. Программистов в хоккейной лиге не оказалось, и Степан решил помочь организаторам —написать программу для определения победителей. В класс HockeyPlayer он решил добавить метод is_lucky(), который будет сравнивать количество букв в имени и возраст; при совпадении этих значений метод is_lucky() вернёт True (это будет означать, что хоккеисту положен приз, например — бесплатный поход к дантисту). Фактически метод is_lucky() возвращает характеристику объекта — и к нему будет удобно обращаться не как к методу, а как к атрибуту. Для этого метод is_lucky() можно представить как свойство экземпляра класса. Свойством называется такой метод, работа с которым подобна работе с атрибутом. Для определения метода как свойства применяют декоратор @property. В интерфейсе экземпляра класса HockeyPlayer свойство is_lucky выглядит как атрибут, а атрибуты экземпляров можно изменять. Однако в существующей реализации изменить его невозможно: Это дефолтное поведение, но при необходимости его можно изменить так, что значение свойства будет доступно для модификации «снаружи», как значение обычного атрибута. Чтобы установить или изменить значение, применяют декоратор @<property_name>.setter, но это тема для отдельной статьи, не будем в неё углубляться. Расширим логику розыгрыша визитов к стоматологу. Добавим класс для описания хоккеистов резервного состава HockeyPlayerPlus и установим в нём дополнительное правило: если хоккеист младше 18 лет — то он безусловно получает путевку к дантисту. Вот повезло так повезло. Решим эту задачу. Создаём класс HockeyPlayerPlus, унаследованный от HockeyPlayer, и в нём переопределяем метод is_lucky(). Однако если хоккеисту 18 лет или больше — для него должен сработать исходный метод is_lucky() из родительского класса. Для обращения к родительскому классу в Python применяется функция super(). Код родительского метода для объекта player_2 отработал без проблем. Степану повезло меньше, чем Кристоферу. Конечно же, это всё — лишь основы ООП и его применения в Python. Объектно-ориентированное программирование  — это не просто технический механизм: это целая парадигма, модель мышления и структурирования программного кода. Даже если вы работаете с языком программирования, который не является чисто объектно-ориентированным (например, Python или JavaScript), знание концепций ООП все равно будет бесценным. Для начинающих разработчиков изучение ООП не только открывает новые горизонты в программировании, но и делает их более конкурентоспособными на рынке труда. Понимание ООП и способность применять его концепции в своем коде — это ожидаемые навыки при найме разработчиков программного обеспечения. Возможно, в начале ООП кажется сложным и замысловатым. Однако есть прямой смысл  экспериментировать, писать код и применять концепции ООП на практике. По мере изучения вы начнете понимать, как эффективно использовать эти инструменты для создания более структурированного, модульного и поддерживаемого кода. Это инвестиция в ваше будущее как разработчика, и она обязательно окупится. А если некоторые из разобранных вопросов так и остались похожими на магию, то более детально разобраться с объектами и классами, а также отработать навыки на конкретных задачах можно на курсе «Python-разработчик» в Практикуме.  Техлид, автор курса Python-разработчик