Объектно-ориентированное программирование | Программирование на Питоне

Объектно-ориентированное программирование

Все курсы > Программирование на Питоне > Занятие 12

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

Единственное, что мы довольно активно применяли — это функции. Они помогали нам выделить повторяющиеся части кода и в дальнейшем просто обращались к ним по имени.

При этом многие языки программирования (в том числе Питон) позволяют использовать подход, который существенно упрощает работу со сложно-структурированным кодом. Такой подход или парадигма называется объектно-ориентированным программированием (object-oriented programming, OOP).

Понятие объектно-ориентированного программирования

Отвлечемся на секунду от программирования. Представьте себе, что вам нужно создать множество фигурок кошек. Хотя все кошки разные, у них все же есть нечто общее.

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

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

Так вот объектно-ориентированное программирование предполагает, что вначале мы создаем шаблон нужного нам предмета (назовем его классом, class), а затем на основе шаблона создаем конкретный предмет (назовем его объектом, object).

объектно-ориентированное программирование: класс и объект

Причем тип животного или цвет шерсти станут атрибутами (attribute) такого объекта, а способность выполнять определенные движения — методами (method).

атрибуты и методы класса

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

Посмотрим как это можно реализовать на Питоне.

Откроем ноутбук к этому занятию

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

Создание класса и метод .__init__()

Класс в Питоне создается с помощью ключевого слова class, названия класса и двоеточия. Внутри класса необходимо прописать метод .__init__(), который будет создавать или инициализировать (initialize) объект этого класса. Методы прописываются через ключевое слово def.

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

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

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

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

Мы создали класс и объект этого класса, однако, к сожалению, пользы этот класс пока никакой не приносит.

Атрибуты класса

Давайте дополним наш класс CatClass атрибутом типа (назовем его type_) и атрибутом цвета шерсти (color).

Названия атрибутов могут быть любыми. При этом, обратите внимание, чтобы избежать конфликта с названием встроенноей функции type(), мы снабдили наш атрибут символом нижнего подчеркивания _.

Методы класса

Дополним наш класс возможностью выполнять определенные действия (то есть создадим методы класса).

Вновь создадим объект и применим имеющиеся в классе методы.

Принципы объектно-ориентированного программирования

Продолжим изучать тему классов и объектов и рассмотрим некоторые принципы объектно-ориентированного программирования.

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

Инкапсуляция (encapsulation) — это способность класса хранить данные и методы внутри себя. Другими словами, объект класса можно представить в виде капсулы, в которой содержатся необходимые данные и методы.

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

С понятием инкапсуляции тесно связаны понятия публичных и частных атрибутов (public and private attributes). Публичные атрибуты (по умолчанию все атрибуты в Питоне публичные) — это те атрибуты, к которым можно получить доступ за пределами «капсулы» класса. Причем не просто получить доступ, но и изменить их.

Согласитесь, это не очень разумно. Нам бы хотелось, чтобы объекты класса CatClass не изменяли значения атрибута type_. Другими словами, нам нужно сделать этот атрибут частным. Здесь есть два способа:

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

К сожалению, это лишь частично решает проблему, потому что атрибут все равно можно изменить.

  • Способ 2. Поставить перед названием атрибута символ двойного подчеркивания. Теперь напрямую получить доступ к этому классу не получится.
ошибка при попытке обратиться к частному атрибуту

К сожалению, и это ограничение можно обойти, поставив _НазваниеКласса перед атрибутом.

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

Принцип наследования (inheritance) предполагает, что один класс наследует атрибуты и методы другого. В этом случае, говорят про Родителя или Суперкласс (parent class, base class) и Потомка или Подкласс (child class, derived class).

наследование в ООП: родительский класс и класс-потомок

Наследование позволяет быстро и без изменения родительского класса дополнять его функционал.

Продолжим простую аналогию с животными, чтобы не думать про логику программы и сосредоточиться на принципах ООП и особенностях синтаксиса.

Создание родительского класса и класса-потомка

Создадим класс Animal, который будет включать в себя наиболее общие свойства и поведение животных: вес и длину (атрибуты), а также способность питаться и спать (методы).

Теперь создадим класс-потомок Bird (птица). В него мы добавим возможность летать (метод).

Создадим объект pigeon (голубь).

Мы можем посмотреть на атрибуты и методы, которые pigeon унаследовал у класса Animal.

Кроме того, мы можем вызвать метод, свойственный только классу Bird.

Функция super()

Обратите внимание, в предыдущем примере класс Bird получил только новые методы, новых атрибутов в нем не появилось. Все дело в том, что если мы хотим добавить атрибут в классе-потомке, сохранив атрибуты родительского класса, нам нужно явным образом вызвать последние с помощью функции super().

Предположим, что в наш класс Bird мы хотим добавить не только метод .move(), но и атрибут flying_speed (скорость полета).

Без функции super() класс Bird не знал бы откуда брать параметры weight и length.

У обновленного класса Bird появились собственные атрибуты.

Как и раньше класс Bird унаследовал методы класса Animal и обзавелся собственным методом .move().

Переопределение класса

Интересной особенностью класса-потомка в Питоне является то, что он переопределяет (по сути, переписывает) родительский класс. Давайте создадим подкласс для нелетающих птиц Flightless, в котором:

  1. Единственным атрибутом будет их скорость бега running_speed
  2. А результат метода .move() мы заменим (что логично) с Flying на Running

Создадим объект ostrich (страус) класса Flightless.

Посмотрим на значение атрибута скорости.

Теперь посмотрим, переопределился ли метод .move().

Важно отметить, что в отличие от атрибутов, которые не наследуются автоматически (как мы видели, для этого нужно использовать функцию super()), методы всех родительских классов (в данном случае Animal —> Bird) передаются потомкам.

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

Питон позволяет классу наследовать методы двух и более классов.

объектно-ориентированный подход: множественное наследование

Предположим, что мы хотим создать класс SwimmingBird (водоплавающая птица) и взять методы плавания и полета у двух разных родительских классов, а именно Fish и Bird.

Вначале создадим родительские классы и необходимые нам методы.

Теперь перейдем к классу-потомку.

Создадим объект duck (утка) класса SwimmingBird.

Как мы видим, утка умеет как летать, так и плавать.

Полиморфизм

Полиморфизм (polymorphism) буквально означает, что один и тот же объект может принимать разные формы. В программировании, полиморфизм предполагает, что операторы, функции и объекты могут взаимодействовать с различными типами данных.

Например, оператор + в случае чисел предполагает сложение, а в случае строк — их объединение.

Полиморфизм функций

Полиморфные функции (polymorphic functions) — это функции, которые могут работать с разными типами данных. Классическим примером является встроенная функция len().

Ее можно применить к строке, списку, словарю или, например, массиву Numpy.

Полиморфизм классов

Полиморфизм классов (class polymorphism) предполагает, что у разных (не связанных друг с другом) классов могут быть методы с одинаковыми названиями.

Пропишем два класса Cat и Dog и наделим их схожими атрибутами и методами:

Создадим объекты этих классов.

Поместим объекты в кортеж и в цикле for вызовем атрибуты и методы каждого из классов.

Парадигма программирования

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

парадигмы программирования: императивный и декларативный подходы

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

Императивное и декларативное программирование

Императивное программирование (imperative programming), как и предполагает его название (от латинского imperare, «властвовать», «повелевать»), явным образом говорит компьютеру, что нужно сделать. Другими словами, мы пишем инструкцию, и компьютер строчка за строчкой ее исполняет.

Декларативный подход (declarative programming) отличается тем, что детали выполнения программы нас не интересуют, для нас важно лишь объяснить компьютеру, какой результат мы хотим получить.

Языки программирования, соответственно, можно разделить на императивные (например, C или Питон) и декларативные (SQL, Haskell). Впрочем, такое деление во многом условно, и чуть дальше я покажу вам, что мы, например, уже использовали декларативную парадигму внутри Питона.

Однако, обо всем по порядку.

Процедурное программирование

Внутри императивного программирования выделяют процедурное программирование (procedural programming). По большом счету, это обычное программирование, которым мы занимались до сегодняшнего занятия. Ставим задачу и последовательно через набор инструкций приходим к нужному решению.

процедурное программирование: последовательность команд

Например, у нас есть список словарей с данными пациентов, и нам нужно посчитать их средний рост.

В соответствии с процедурным подходом мы могли бы использовать циклы for.

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

Здесь на помощь приходят классы.

Объектно-ориентированное программирование

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

создание объектов класса на Питоне

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

Предложенную задачу о среднем росте можно решить и с помощью класса.

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

Функциональное программирование

По большому счету, функциональное программирование (functional programming) — это набор функций, которые последовательно решают поставленную задачу. Результат работы одной функции становится входящим параметром для другой.

функциональное программирование

Преимуществом является то, что вы четко разделяете функции, их параметры и передаваемые им данные. Это зачастую упрощает понимание логики программы и поиск ошибок.

Решение через функциональный подход

В частности, решим поставленную выше задачу, вначале последовательно применив несколько функций (lambda-функцию, функцию map() и функцию list()) к словарю с данными о пациентах.

После получения значений роста остается применить функции sum() и len() для расчета среднего значения.

Функция einsum()

Еще одним примером функционального программирования является функция einsum(). В данном случае мы берем два массива и описываем по каким правилам хотим их преобразовать.

Функциональное программирование на R

R — это декларативный функциональный язык, специально созданный для обработки данных и статистических исследований. Если Питон в основном применяется для решения задач бизнеса, то R чаще используется в науке.

На языке R большая часть задач решается с помощью функций, которые последовательно преобразуют передаваемые им данные. Рассмотрим вот такой пример подсчета количества мальчиков по имени Тейлор в датасете babynames.

Даже если вы первый раз видите код на R довольно несложно догадаться, что в данном случае мы (1) с помощью функции filter() находим мальчиков по имени Тейлор в датасете babynames, затем (2) отбираем только эти строки через select() и (3) наконец складываем результат.

Ту же самую задачу можно реализовать через еще более «функциональный» код с использованием оператора %>% (pipeline operator).

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

Для первого знакомства с парадигмами программирования полученных знаний будет достаточно. Логическое и математическое (линейное и нелинейное) программирование останутся за рамками сегодняшнего занятия.

Классы и объекты в машинном обучении

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

Готовые классы библиотеки sklearn

На самом деле мы уже активно использовали классы и объекты, когда работали, в частности, с библиотекой sklearn. Эта библиотека состоит из классов, которые способны решать самые разные задачи (и здесь, надо сказать, объектно-ориентированная парадигма подходит как нельзя лучше).

Напомню про некоторые из них:

Полный перечень можно найти в документации⧉ библиотеки sklearn.

Вспомним, как применялся, например, класс LinearRegression. Возьмем знакомые нам данные роста и обхвата шеи.

Как мы помним sklearn требует, чтобы признаки содержались в двумерном массиве.

Теперь импортиртируем класс LinearRegression и создадим объект этого класса.

Обучим модель с помощью метода .fit() и посмотрим на коэффициенты (атрибуты).

Теперь сделаем прогноз через .predict().

Пример ООП: собственный класс линейной регрессии

Давайте дополнительно попрактикуемся в создании классов и реализуем модель линейной регрессии, аналогичную классу LinearRegression в библиотеке sklearn. В частности, создадим класс, который мы назовем SimpleLinearRegression, для нахождения коэффициентов простой линейной регрессии. Напомню, уравнение простой линейной регрессии имеет вид

$$ y = w \cdot x + b $$

Для уравнения с одной переменной аналитическое решение (closed-form solution) может быть найдено через метод наименьших квадратов (МНК, least squares method) по формулам ниже

$$ w = \frac {\sum_{i = 1}^{n} (x_i-\bar{x})(y_i-\bar{y})} {\sum_{i = 1}^{n} (x_i-\bar{x})^2} $$

$$ b = \bar{y}-w\bar{x} $$

Подробнее про метод наименьших квадратов мы будем говорить на следующих курсах.

Напишем класс SimpleLinearRegression и снабдим его методами .fit() и .predict().

Создадим объект этого класса.

Обучим модель и посмотрим на коэффициенты.

Остается сделать прогноз.

Пара важных деталей

Обратим внимание на два нюанса, при несоблюдении которых Питон может выдать ошибку.

Атрибуты класса и локальные переменные методов

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

Только теперь значения атрибутов доступны любому методу в классе. В частности, мы передали их методу .predict() для создания прогноза. Если бы мы забыли прописать код выше, метод .predict() не получил бы необходимых ему данных.

Вызов метода внутри другого метода в классе

Внутри метода .fit() мы вызываем еще один метод .find_mean() для нахождения среднего арифметического значения. Очень важно, чтобы «вложенный» метод вызывался через self.

В противном случае Питоне не будет знать, где искать нужный метод, и код выдаст ошибку.

Подведем итог

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

Помимо этого мы узнали в целом о том, какие подходы или парадигмы используются при написании кода.

Вопросы для закрепления

Вопрос. Что означает запись названия атрибута _type_ и __type_?

Ответ: одинарный и двойной символы подчеркивания перед названием атрибута указывают на то, что это частные атрибуты (private attributes), которые должны использоваться только внутри класса (одинарный символ рекомендует не обращаться к атрибуту type извне, двойной — препятствует этому); символ подчеркивания после названия атрибута позволяет отличить переменную type от названия функции type()

Вопрос. Наследуются ли методы и атрибуты родительского класса?

Ответ: (1) методы родительского класса наследуются по умолчанию, (2) то же относится и к атрибутам, при условии что в классе-потомке новых атрибутов не будет; (3) при этом для того чтобы унаследовать атрибуты родительского класса и одновременно добавить свои собственные, в классе-потомке нужно использовать функцию super()

Вопрос. В чем смысл функционального подхода к программированию?

Ответ: получение результата достигается за счет передачи данных из одной функции в другую

На следующем занятии мы вернемся «на уровень ниже» и посмотрим как пишутся и исполняются программы на Питоне.