Все курсы > Программирование на Питоне > Занятие 12
До сих пор на этом курсе мы усиленно изучали программирование на Питоне, но практически не задумывались над тем, как мы программируем и как мы организовываем код, который пишем. Все потому, что код, который мы создавали был очень прост.
Единственное, что мы довольно активно применяли — это функции. Они помогали нам выделить повторяющиеся части кода и в дальнейшем просто обращались к ним по имени.
При этом многие языки программирования (в том числе Питон) позволяют использовать подход, который существенно упрощает работу со сложно-структурированным кодом. Такой подход или парадигма называется объектно-ориентированным программированием (object-oriented programming, OOP).
Понятие объектно-ориентированного программирования
Отвлечемся на секунду от программирования. Представьте себе, что вам нужно создать множество фигурок кошек. Хотя все кошки разные, у них все же есть нечто общее.
- Во-первых, у них есть общие свойства, например, шерсть, когти и хвост.
- Кроме того, любая кошка может выполнять определенные действия: лазить по деревьям, мяукать и потягиваться.
Если мы хотим максимально сэкономить время при рисовании кошек, то будет логично создать шаблон условной кошки, а затем копировать этот шаблон и дополнять нужными нам свойствами и положениями.
Так вот объектно-ориентированное программирование предполагает, что вначале мы создаем шаблон нужного нам предмета (назовем его классом, class), а затем на основе шаблона создаем конкретный предмет (назовем его объектом, object).

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

По большому счету в этом и заключается суть программирования, ориентированного на объект. Наши программы теперь состоят не из последовательных команд, а представляют собой объекты.
Посмотрим как это можно реализовать на Питоне.
Откроем ноутбук к этому занятию⧉
Классы и объекты в Питоне
Создание класса и метод .__init__()
Класс в Питоне создается с помощью ключевого слова class, названия класса и двоеточия. Внутри класса необходимо прописать метод .__init__(), который будет создавать или инициализировать (initialize) объект этого класса. Методы прописываются через ключевое слово def.
Дополнительно метод .__init__() нужно снабдить аргументом, его принято обозначать словом self, который при создании объекта будет ссылаться на этот же объект (то есть по сути сам на себя, отсюда и выбор такого названия).
1 2 3 4 5 6 |
# создадим класс CatClass class CatClass: # и пропишем метод .__init__() def __init__(self): pass |
Оператор pass мы указали, потому что метод класса, как и тело функции, нельзя оставлять пустым. Теперь давайте создадим или инициализируем объект созданного класса.
Создание объекта
Для того чтобы создать объект, запишем наш класс в переменную. Пусть это будет Matroskin. Дополнительно проверим тип данных новой переменной.
1 2 3 4 5 |
# создадим объект Matroskin класса CatClass() Matroskin = CatClass() # проверим тип данных созданной переменной type(Matroskin) |
1 |
__main__.CatClass |
Мы создали класс и объект этого класса, однако, к сожалению, пользы этот класс пока никакой не приносит.
Атрибуты класса
Давайте дополним наш класс CatClass атрибутом типа (назовем его type_) и атрибутом цвета шерсти (color).
1 2 3 4 5 6 7 8 9 10 11 |
# вновь создадим класс CatClass class CatClass: # метод .__init__() на этот раз принимает еще и параметр color def __init__(self, color): # этот параметр будет записан в переменную атрибута с таким же названием self.color = color # значение атрибута type_ задается внутри класса self.type_ = 'cat' |
Названия атрибутов могут быть любыми. При этом, обратите внимание, чтобы избежать конфликта с названием встроенноей функции type(), мы снабдили наш атрибут символом нижнего подчеркивания _.
1 2 3 4 5 |
# повторно создадим объект класса CatClass, передав ему параметр цвета шерсти Matroskin = CatClass('gray') # и выведем атрибуты класса Matroskin.color, Matroskin.type_ |
1 |
('gray', 'cat') |
Методы класса
Дополним наш класс возможностью выполнять определенные действия (то есть создадим методы класса).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# перепишем класс CatClass class CatClass: # метод .__init__() и атрибуты оставим без изменений def __init__(self, color): self.color = color self.type_ = 'cat' # однако добавим метод, который позволит коту мяукать def meow(self): for i in range(3): print('Мяу') # и метод .info() для вывода информации об объекте def info(self): print(self.color, self.type_) |
Вновь создадим объект и применим имеющиеся в классе методы.
1 2 |
# создадим объект Matroskin = CatClass('gray') |
1 2 |
# применим метод .meow() Matroskin.meow() |
1 2 3 |
Мяу Мяу Мяу |
1 2 |
# и метод .info() Matroskin.info() |
1 |
gray cat |
Принципы объектно-ориентированного программирования
Продолжим изучать тему классов и объектов и рассмотрим некоторые принципы объектно-ориентированного программирования.
Инкапсуляция
Инкапсуляция (encapsulation) — это способность класса хранить данные и методы внутри себя. Другими словами, объект класса можно представить в виде капсулы, в которой содержатся необходимые данные и методы.
Публичные и частные атрибуты класса
С понятием инкапсуляции тесно связаны понятия публичных и частных атрибутов (public and private attributes). Публичные атрибуты (по умолчанию все атрибуты в Питоне публичные) — это те атрибуты, к которым можно получить доступ за пределами «капсулы» класса. Причем не просто получить доступ, но и изменить их.
1 2 3 4 5 |
# изменим атрибут type_ объекта Matroskin на dog Matroskin.type_ = 'dog' # выведем этот атрибут Matroskin.type_ |
1 |
'dog' |
Согласитесь, это не очень разумно. Нам бы хотелось, чтобы объекты класса CatClass не изменяли значения атрибута type_. Другими словами, нам нужно сделать этот атрибут частным. Здесь есть два способа:
- Способ 1. Поставить один символ подчеркивания перед атрибутом и, таким образом, сообщить тем, кто будет пользоваться нашим кодом, что это частный атрибут.
1 2 3 4 5 6 7 |
class CatClass: def __init__(self, color): self.color = color # символ подчеркивания ПЕРЕД названием атрибута указывает, # что это частный атрибут и изменять его не стоит self._type_ = 'cat' |
К сожалению, это лишь частично решает проблему, потому что атрибут все равно можно изменить.
1 2 3 4 5 6 |
# вновь создадим объект класса CatClass Matroskin = CatClass('gray') # и изменим значение атрибута _type_ Matroskin._type_ = 'dog' Matroskin._type_ |
1 |
'dog' |
- Способ 2. Поставить перед названием атрибута символ двойного подчеркивания. Теперь напрямую получить доступ к этому классу не получится.
1 2 3 4 5 6 |
class CatClass: def __init__(self, color): self.color = color # символ двойного подчеркивания предотвратит доступ извне self.__type_ = 'cat' |
1 2 3 4 |
Matroskin = CatClass('gray') # теперь при вызове этого атрибута Питон выдаст ошибку Matroskin.__type_ |

К сожалению, и это ограничение можно обойти, поставив _НазваниеКласса перед атрибутом.
1 2 3 4 5 |
# поставим _CatClass перед __type_ Matroskin._CatClass__type_ = 'dog' # к сожалению, значение атрибута изменится Matroskin._CatClass__type_ |
1 |
'dog' |
Наследование
Принцип наследования (inheritance) предполагает, что один класс наследует атрибуты и методы другого. В этом случае, говорят про Родителя или Суперкласс (parent class, base class) и Потомка или Подкласс (child class, derived class).

Наследование позволяет быстро и без изменения родительского класса дополнять его функционал.
Продолжим простую аналогию с животными, чтобы не думать про логику программы и сосредоточиться на принципах ООП и особенностях синтаксиса.
Создание родительского класса и класса-потомка
Создадим класс Animal, который будет включать в себя наиболее общие свойства и поведение животных: вес и длину (атрибуты), а также способность питаться и спать (методы).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# создадим класс Animal class Animal: # пропишем метод .__init__() с двумя параметрами: вес (кг) и длина (см) def __init__(self, weight, length): # поместим аргументы этих параметров в соответствующие переменные self.weight = weight self.length = length # объявим метод .eat() def eat(self): print('Eating') # и .sleep() def sleep(self): print('Sleeping') |
Теперь создадим класс-потомок Bird (птица). В него мы добавим возможность летать (метод).
1 2 3 4 5 6 7 8 9 |
# создадим класс Bird # родительский класс Animal пропишем в скобках class Bird(Animal): # внутри класса Bird объявим новый метод .move() def move(self): # для птиц .move() будет означать "летать" print('Flying') |
Создадим объект pigeon (голубь).
1 2 |
# создадим объект pigeon и передадим ему значения веса и длины pigeon = Bird(0.3, 30) |
Мы можем посмотреть на атрибуты и методы, которые pigeon унаследовал у класса Animal.
1 2 |
# посмотрим на унаследованные у класса Animal атрибуты pigeon.weight, pigeon.length |
1 |
(0.3, 30) |
1 2 |
# и методы pigeon.eat() |
1 |
Eating |
Кроме того, мы можем вызвать метод, свойственный только классу Bird.
1 |
pigeon.move() |
1 |
Flying |
Функция super()
Обратите внимание, в предыдущем примере класс Bird получил только новые методы, новых атрибутов в нем не появилось. Все дело в том, что если мы хотим добавить атрибут в классе-потомке, сохранив атрибуты родительского класса, нам нужно явным образом вызвать последние с помощью функции super().
Предположим, что в наш класс Bird мы хотим добавить не только метод .move(), но и атрибут flying_speed (скорость полета).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# снова создадим класс Bird class Bird(Animal): # в метод .__init__() добавим параметр скорости полета (км/ч) def __init__(self, weight, length, flying_speed): # с помощью функции super() вызовем метод .__init__() родительского класса Animal super().__init__(weight, length) self.flying_speed = flying_speed # вновь пропишем метод .move() def move(self): print('Flying') |
Без функции super() класс Bird не знал бы откуда брать параметры weight и length.
1 2 |
# вновь создадим объект pigeon класса Bird, но уже с тремя параметрами pigeon = Bird(0.3, 30, 100) |
У обновленного класса Bird появились собственные атрибуты.
1 2 |
# вызовем как унаследованные, так и собственные атрибуты класса Bird pigeon.weight, pigeon.length, pigeon.flying_speed |
1 |
(0.3, 30, 100) |
Как и раньше класс Bird унаследовал методы класса Animal и обзавелся собственным методом .move().
1 2 |
# вызовем унаследованный метод .sleep() pigeon.sleep() |
1 |
Sleeping |
1 2 |
# и собственный метод .move() pigeon.move() |
1 |
Flying |
Переопределение класса
Интересной особенностью класса-потомка в Питоне является то, что он переопределяет (по сути, переписывает) родительский класс. Давайте создадим подкласс для нелетающих птиц Flightless, в котором:
- Единственным атрибутом будет их скорость бега running_speed
- А результат метода .move() мы заменим (что логично) с Flying на Running
1 2 3 4 5 6 7 8 9 10 11 12 |
# создадим подкласс Flightless класса Bird class Flightless(Bird): # метод .__init__() этого подкласса "стирает" .__init__() родительского класса def __init__(self, running_speed): # таким образом, у нас остается только один атрибут self.running_speed = running_speed # кроме того, результатом метода .move() будет 'Running' def move(self): print('Running') |
Создадим объект ostrich (страус) класса Flightless.
1 |
ostrich = Flightless(60) |
Посмотрим на значение атрибута скорости.
1 2 |
# страусы бегают довольно быстро ostrich.running_speed |
1 |
60 |
Теперь посмотрим, переопределился ли метод .move().
1 |
ostrich.move() |
1 |
Running |
Важно отметить, что в отличие от атрибутов, которые не наследуются автоматически (как мы видели, для этого нужно использовать функцию super()), методы всех родительских классов (в данном случае Animal —> Bird) передаются потомкам.
1 2 |
# применим метод .eat() класса Animal ostrich.eat() |
1 |
Eating |
Множественное наследование
Питон позволяет классу наследовать методы двух и более классов.

Предположим, что мы хотим создать класс SwimmingBird (водоплавающая птица) и взять методы плавания и полета у двух разных родительских классов, а именно Fish и Bird.
Вначале создадим родительские классы и необходимые нам методы.
1 2 3 4 5 6 |
# создадим родительский класс Fish class Fish: # и метод .swim() def swim(self): print('Swimming') |
1 2 3 4 5 6 |
# и еще один родительский класс Bird class Bird: # и метод .fly() def fly(self): print('Flying') |
Теперь перейдем к классу-потомку.
1 2 3 |
# родительские классы мы перечисляем в скобках через зяпятую class SwimmingBird(Bird, Fish): pass |
Создадим объект duck (утка) класса SwimmingBird.
1 |
duck = SwimmingBird() |
Как мы видим, утка умеет как летать, так и плавать.
1 |
duck.fly() |
1 |
Flying |
1 |
duck.swim() |
1 |
Swimming |
Полиморфизм
Полиморфизм (polymorphism) буквально означает, что один и тот же объект может принимать разные формы. В программировании, полиморфизм предполагает, что операторы, функции и объекты могут взаимодействовать с различными типами данных.
Например, оператор + в случае чисел предполагает сложение, а в случае строк — их объединение.
1 2 |
# для чисел '+' является оператором сложения 2 + 2 |
1 |
4 |
1 2 |
# для строк - оператором объединения 'классы' + ' и ' + 'объекты' |
1 |
'классы и объекты' |
Полиморфизм функций
Полиморфные функции (polymorphic functions) — это функции, которые могут работать с разными типами данных. Классическим примером является встроенная функция len().
Ее можно применить к строке, списку, словарю или, например, массиву Numpy.
1 |
len('Программирование на Питоне') |
1 |
26 |
1 |
len(['Программирование', 'на', 'Питоне']) |
1 |
3 |
1 |
len({0 : 'Программирование', 1 : 'на', 2 : 'Питоне'}) |
1 |
3 |
1 2 |
import numpy as np len(np.array([1, 2, 3])) |
1 |
3 |
Полиморфизм классов
Полиморфизм классов (class polymorphism) предполагает, что у разных (не связанных друг с другом) классов могут быть методы с одинаковыми названиями.
Пропишем два класса Cat и Dog и наделим их схожими атрибутами и методами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# создадим класс котов class CatClass: # определим атрибуты клички, типа и цвета шерсти def __init__(self, name, color): self.name = name self._type_ = 'кот' self.color = color # создадим метод .info() для вывода этих атрибутов def info(self): print(f'Меня зовут {self.name}, я {self._type_}, цвет моей шерсти {self.color}') # и метод .sound(), показывающий, что коты умеют мяукать def sound(self): print('Я умею мяукать') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# создадим класс собак class DogClass: # с такими же атрибутами def __init__(self, name, color): self.name = name self._type_ = 'пес' self.color = color # и методами def info(self): print(f'Меня зовут {self.name}, я {self._type_}, цвет моей шерсти {self.color}') # хотя, обратите внимание, действия внутри методов отличаются def sound(self): print('Я умею лаять') |
Создадим объекты этих классов.
1 2 |
cat = CatClass('Бегемот', 'черный') dog = DogClass('Барбос', 'серый') |
Поместим объекты в кортеж и в цикле for вызовем атрибуты и методы каждого из классов.
1 2 3 4 |
for animal in (cat, dog): animal.info() animal.sound() print() |
1 2 3 4 5 |
Меня зовут Бегемот, я кот, цвет моей шерсти черный Я умею мяукать Меня зовут Барбос, я пес, цвет моей шерсти серый Я умею лаять |
Парадигма программирования
Парадигма программирования — это, по большому счету, способ организации и стиль написания кода. Создание различных парадигм необходимо для того, чтобы справиться со все возрастающей сложностью компьютерных программ.

Как вы видите на схеме выше, парадигмы программирования можно разделить на две большие группы: императивное и декларативное программирование.
Императивное и декларативное программирование
Императивное программирование (imperative programming), как и предполагает его название (от латинского imperare, «властвовать», «повелевать»), явным образом говорит компьютеру, что нужно сделать. Другими словами, мы пишем инструкцию, и компьютер строчка за строчкой ее исполняет.
Декларативный подход (declarative programming) отличается тем, что детали выполнения программы нас не интересуют, для нас важно лишь объяснить компьютеру, какой результат мы хотим получить.
Языки программирования, соответственно, можно разделить на императивные (например, C или Питон) и декларативные (SQL, Haskell). Впрочем, такое деление во многом условно, и чуть дальше я покажу вам, что мы, например, уже использовали декларативную парадигму внутри Питона.
Однако, обо всем по порядку.
Процедурное программирование
Внутри императивного программирования выделяют процедурное программирование (procedural programming). По большом счету, это обычное программирование, которым мы занимались до сегодняшнего занятия. Ставим задачу и последовательно через набор инструкций приходим к нужному решению.

Например, у нас есть список словарей с данными пациентов, и нам нужно посчитать их средний рост.
1 2 3 |
patients = [{'name': 'Николай', 'height': 178}, {'name': 'Иван', 'height': 182}, {'name': 'Алексей', 'height': 190}] |
В соответствии с процедурным подходом мы могли бы использовать циклы for.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# создадим переменные для общего роста и количества пациентов total, count = 0, 0 # в цикле for пройдемся по пациентам (отдельным словарям) for patient in patients: # достанем значение роста и прибавим к текущему значению переменной total total += patient['height'] # на каждой итерации будем увеличивать счетчик пациентов на один count += 1 # разделим общий рост на количество пациентов, # чтобы получить среднее значение total / count |
1 |
183.33333333333334 |
Впрочем, когда кода и людей его разрабатывающих становится слишком много, мы довольно быстро сталкиваемся с недостатками такого подхода. Структура кода становится не очевидной, найти ошибку бывает все сложнее и сложнее.
Здесь на помощь приходят классы.
Объектно-ориентированное программирование
В мире ООП все задачи решаются не одной большой программой, а классами (и созданными на их основе объектами). Под каждую задачу создается, как правило, отдельный класс. И хотя поначалу использование классов может показаться довольно сложным, на самом деле это существенно упрощает решение многих задач.

Более того, изученные выше принципы объектно-ориентированного подхода, а именно инкапсуляция, наследование и полиморфизм еще больше способствуют грамотной организации кода.
Предложенную задачу о среднем росте можно решить и с помощью класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# создадим класс для работы с данными DataClass class DataClass: # при создании объекта будем передавать ему данные для анализа def __init__(self, data): self.data = data # кроме того, создадим метод для расчета среднего значения def count_average(self, metric): # параметр metric определит по какому столбцу считать среднее self.metric = metric # объявим два частных атрибута self.__total = 0 self.__count = 0 # в цикле for пройдемся по списку словарей for item in self.data: # рассчитем общую сумму по указанному в metric # значению каждого словаря self.__total += item[self.metric] # и количество таких записей self.__count += 1 # разделим общую сумму показателя на количество записей return self.__total / self.__count |
1 2 3 4 5 |
# создадим объект класса DataClass и передадим ему данные о пациентах data_object = DataClass(patients) # вызовем метод .count_average() с метрикой 'height' data_object.count_average('height') |
1 |
183.33333333333334 |
Впрочем, вы возможно заметили, что в данном случае использование класса не выглядит слишком логично. Задачи по обработке и анализу данных довольно удобно решать с помощью функционального программирования.
Функциональное программирование
По большому счету, функциональное программирование (functional programming) — это набор функций, которые последовательно решают поставленную задачу. Результат работы одной функции становится входящим параметром для другой.

Преимуществом является то, что вы четко разделяете функции, их параметры и передаваемые им данные. Это зачастую упрощает понимание логики программы и поиск ошибок.
Решение через функциональный подход
В частности, решим поставленную выше задачу, вначале последовательно применив несколько функций (lambda-функцию, функцию map() и функцию list()) к словарю с данными о пациентах.
1 2 3 4 5 |
# lambda-функция достанет значение по ключу height # функция map() применит lambda-функцию к каждому вложенному в patients словарю # функция list() преобразует результат в список heights = list(map(lambda x: x['height'], patients)) heights |
1 |
[178, 182, 190] |
После получения значений роста остается применить функции sum() и len() для расчета среднего значения.
1 |
sum(heights) / len(heights) |
1 |
183.33333333333334 |
Функция einsum()
Еще одним примером функционального программирования является функция einsum(). В данном случае мы берем два массива и описываем по каким правилам хотим их преобразовать.
1 2 3 4 5 6 7 |
# возьмем два двумерных массива a = np.array([[0, 1, 2], [3, 4, 5]]) b = np.array([[5, 4], [3, 2], [1, 0]]) |
1 2 |
# перемножим a и b по индексу j через функцию np.einsum() np.einsum('ij, jk -> ik', a, b) |
1 2 |
array([[ 5, 2], [32, 20]]) |
Функциональное программирование на R
R — это декларативный функциональный язык, специально созданный для обработки данных и статистических исследований. Если Питон в основном применяется для решения задач бизнеса, то R чаще используется в науке.
На языке R большая часть задач решается с помощью функций, которые последовательно преобразуют передаваемые им данные. Рассмотрим вот такой пример подсчета количества мальчиков по имени Тейлор в датасете babynames.
1 |
sum(select(filter(babynames, sex == "M", name == "Taylor"), n)) |
Даже если вы первый раз видите код на R довольно несложно догадаться, что в данном случае мы (1) с помощью функции filter() находим мальчиков по имени Тейлор в датасете babynames, затем (2) отбираем только эти строки через select() и (3) наконец складываем результат.
Ту же самую задачу можно реализовать через еще более «функциональный» код с использованием оператора %>% (pipeline operator).
1 2 3 4 |
# в таком виде последовательность становится еще более наглядной babynames %>% filter(sex == "M", name == "Taylor") %>% select(n) %>% sum |
По сути мы создали пайплайн (то есть последовательность операций), где каждый шаг решается определенной функцией.
Для первого знакомства с парадигмами программирования полученных знаний будет достаточно. Логическое и математическое (линейное и нелинейное) программирование останутся за рамками сегодняшнего занятия.
Классы и объекты в машинном обучении
Мы только что изучили, как объектно-ориентированное программирование встраивается в другие парадигмы. Теперь давайте посмотрим, как применять ООП в машинном обучении.
Готовые классы библиотеки sklearn
На самом деле мы уже активно использовали классы и объекты, когда работали, в частности, с библиотекой sklearn. Эта библиотека состоит из классов, которые способны решать самые разные задачи (и здесь, надо сказать, объектно-ориентированная парадигма подходит как нельзя лучше).
Напомню про некоторые из них:
- На занятии по линейной регрессии мы использовали класс LinearRegression;
- Изучая классификацию, применяли класс LogisticRegression;
- В рамках кластерного анализа нам понадобился класс KMeans.
Полный перечень можно найти в документации⧉ библиотеки sklearn.
Вспомним, как применялся, например, класс LinearRegression. Возьмем знакомые нам данные роста и обхвата шеи.
1 2 |
X = np.array([1.48, 1.49, 1.49, 1.50, 1.51, 1.52, 1.52, 1.53, 1.53, 1.54, 1.55, 1.56, 1.57, 1.57, 1.58, 1.58, 1.59, 1.60, 1.61, 1.62, 1.63, 1.64, 1.65, 1.65, 1.66, 1.67, 1.67, 1.68, 1.68, 1.69, 1.70, 1.70, 1.71, 1.71, 1.71, 1.74, 1.75, 1.76, 1.77, 1.77, 1.78]) y = np.array([29.1, 30.0, 30.1, 30.2, 30.4, 30.6, 30.8, 30.9, 31.0, 30.6, 30.7, 30.9, 31.0, 31.2, 31.3, 32.0, 31.4, 31.9, 32.4, 32.8, 32.8, 33.3, 33.6, 33.0, 33.9, 33.8, 35.0, 34.5, 34.7, 34.6, 34.2, 34.8, 35.5, 36.0, 36.2, 36.3, 36.6, 36.8, 36.8, 37.0, 38.5]) |
Как мы помним sklearn требует, чтобы признаки содержались в двумерном массиве.
1 2 |
# преобразуем данные роста в двумерный массив X_2D = X.reshape(-1, 1) |
Теперь импортиртируем класс LinearRegression и создадим объект этого класса.
1 2 3 4 5 |
# из набора линейных моделей библиотеки sklearn импортируем линейную регрессию from sklearn.linear_model import LinearRegression # создадим объект этого класса и запишем в переменную model model = LinearRegression() |
Обучим модель с помощью метода .fit() и посмотрим на коэффициенты (атрибуты).
1 2 3 4 5 |
# обучим модель с помощью метода .fit(), которому передадим наши данные model.fit(X_2D, y) # на выходе получим коэффициенты линейной регрессии model.coef_, model.intercept_ |
1 |
(array([26.86181201]), -10.570936299787334) |
Теперь сделаем прогноз через .predict().
1 2 3 |
# построим прогноз и выведем первые пять значений y_pred = model.predict(X_2D) y_pred[:5] |
1 |
array([29.18454547, 29.45316359, 29.45316359, 29.72178171, 29.99039983]) |
Пример ООП: собственный класс линейной регрессии
Давайте дополнительно попрактикуемся в создании классов и реализуем модель линейной регрессии, аналогичную классу 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().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# нашему классу понадобится Numpy import numpy as np # создадим класс SimpleLinearRegression class SimpleLinearRegression: # в методе .__init__() объявим переменные наклона и сдвига def __init__(self): self.slope_ = None self.intercept_ = None # создадим метод .fit() def fit(self, X, y): # найдем среднее значение X и y X_mean = self.find_mean(X) y_mean = self.find_mean(y) # объявим переменные для числителя и знаменателя numerator, denominator = 0, 0 # в цикле пройдемся по данным for i in range(len(X)): # вычислим значения числителя и знаменателя по формуле выше numerator += (X[i] - X_mean) * (y[i] - y_mean) denominator += (X[i] - X_mean) ** 2 # найдем наклон и сдвиг slope_ = numerator / denominator intercept_ = y_mean - slope_ * X_mean # сохраним получившиеся коэффициенты в виде атрибутов self.slope_ = slope_ self.intercept_ = intercept_ # метод .predict() просто умножит через скалярное произведение # вектор данных на наклон и прибавит сдвиг def predict(self, X): # на выходе мы получим вектор прогнозных значений return np.dot(self.slope_, X) + self.intercept_ # служебная функция: расчет среднего def find_mean(self, nums): return sum(nums)/len(nums) |
Создадим объект этого класса.
1 2 |
# создадим объект класса SimpleLinearRegression model = SimpleLinearRegression() |
Обучим модель и посмотрим на коэффициенты.
1 2 3 4 5 |
# применим метод .fit() model.fit(X, y) # посмотрим на коэффициенты model.slope_, model.intercept_ |
1 |
(26.861812005569753, -10.570936299787313) |
Остается сделать прогноз.
1 2 3 4 5 |
# сделаем прогноз через .predict() y_pred = model.predict(X) # и выведем первые пять коэффициентов y_pred[:5] |
1 |
array([29.18454547, 29.45316359, 29.45316359, 29.72178171, 29.99039983]) |
Пара важных деталей
Обратим внимание на два нюанса, при несоблюдении которых Питон может выдать ошибку.
Атрибуты класса и локальные переменные методов
В методе .__init__() мы объявили два атрибута класса .self.slope_ и .self.intercept_. После этого в методе .fit() мы объявили локальные переменные slope_ и intercept_. Рассчитав сдвиг и наклон прямой, мы записали эти значения в атрибуты класса.
1 2 |
self.slope_ = slope_ self.intercept_ = intercept_ |
Только теперь значения атрибутов доступны любому методу в классе. В частности, мы передали их методу .predict() для создания прогноза. Если бы мы забыли прописать код выше, метод .predict() не получил бы необходимых ему данных.
Вызов метода внутри другого метода в классе
Внутри метода .fit() мы вызываем еще один метод .find_mean() для нахождения среднего арифметического значения. Очень важно, чтобы «вложенный» метод вызывался через self.
1 |
X_mean = self.find_mean(X) |
В противном случае Питоне не будет знать, где искать нужный метод, и код выдаст ошибку.
Подведем итог
Сегодня мы узнали, что такое объектно-ориентированное программирование, научились создавать классы и объекты этих классов, рассмотрели принципы ООП (инкапсуляцию, наследование и полиморфизм). Кроме того, мы увидели, как классы и объекты применяются в машинном обучении.
Помимо этого мы узнали в целом о том, какие подходы или парадигмы используются при написании кода.
Вопросы для закрепления
Вопрос. Что означает запись названия атрибута _type_ и __type_?
Посмотреть правильный ответ
Ответ: одинарный и двойной символы подчеркивания перед названием атрибута указывают на то, что это частные атрибуты (private attributes), которые должны использоваться только внутри класса (одинарный символ рекомендует не обращаться к атрибуту type извне, двойной — препятствует этому); символ подчеркивания после названия атрибута позволяет отличить переменную type от названия функции type()
Вопрос. Наследуются ли методы и атрибуты родительского класса?
Посмотреть правильный ответ
Ответ: (1) методы родительского класса наследуются по умолчанию, (2) то же относится и к атрибутам, при условии что в классе-потомке новых атрибутов не будет; (3) при этом для того чтобы унаследовать атрибуты родительского класса и одновременно добавить свои собственные, в классе-потомке нужно использовать функцию super()
Вопрос. В чем смысл функционального подхода к программированию?
Посмотреть правильный ответ
Ответ: получение результата достигается за счет передачи данных из одной функции в другую
На следующем занятии мы вернемся «на уровень ниже» и посмотрим как пишутся и исполняются программы на Питоне.