Python - Объектно Ориентированное Программирование (ООП)
В данной статье даются основы ООП в питоне
В python всё - объекты.
*Аудитория в шоке, особо нервные дамочки падают в обморок*- Числа - объекты
- Строки - объекты
- Списки - объекты
- Классы - объекты
- ...
Если говорить просто, то "объекты" - это некая структура.
И было слово...
Чтобы создавать объекты, используются "конструкторы" или классы.- Класс - это схема, описывающая нашу структуру, возможные внутри неё данные и присущие ей методы.
- Метод - это функция. Т.е. метод объекта - это функция, описанная внутри объекта, и присущая этому объекту. Метод - это функция, которая действует на объекты данного вида. Для удобства у разных видов объектов могут быть методы с одинаковыми именами, работающие по разному, но схожим образом.
- Экземпляр - это конкретный объект, созданный из класса.
Рассмотрим пример
Список:L = [1, 2, 3, 4]
- Список (List) - это класс объекта
- Переменная L содержит экземпляр объекта (конкретно список [1, 2, 3, 4])
- append(), sort() - методы объекта, т.е. функции, которые можно применить к его экземплярам.
Пока звучит не очень сложно.
Но по прежнему не понятно зачем это нужно. =)
1 что приходит на ум - абстракция. Мы не думаем о том как устроен объект, а думаем о том что мы можем с ним сделать.
Например, возьмём функцию dir() и передадим экземпляр объекта список в неё как аргумент:
dir(L)
На выходе получим большой список. Большой список методов для списка.
У нас есть 2 пути:
- опробовать некоторые из них (возможно, пытаясь передать какие-то атрибуты)
- найти их описание в google по имени
Но в любом случае мы узнаем о том, "что можно делать со списком", не изучая то как он устроен.
При этом если мы создадим свою структуру данных, свой класс со своими объектами, то у них могут быть одноимённые методы (например, если они ведут себя аналогично).
Например, у строк есть несколько методов, одноимённых методом списков:
Метод index() является одним из них.
Рассмотрим как он работает для наших примеров:
Легко заметить, что некоторые методы в списке имели по 2 подчёркивания с обеих сторон, например:
__len__Это "стандартный" метод. Фактически, когда мы используем функцию len(), например:
то на самом деле вызывается метод __len__ соответствующего объекта.
Почему?
Всё потому же - если мы будем внутри функции языка len описывать как вычислять длину любого объекта, то это будет очень много кода. Да и для новых классов объектов (например, numpy.array) эта функция не будет работать.
А так у каждого класса внутри будет краткое описание того как это работает.
Так же это позволяет переопределить поведение некоторых операторов.
Например, что будет, если мы напишем:
Фактически будет вызван метод add - s.__add__(L)
В нашем примере мы получим ошибку:
Но некоторые классы объектов вполне могут принимать на вход "чужака" (не всякого конечно):
А ещё когда мы пишем
print L
, то на самом деле вызывается метод __str__, чтобы преобразовать список L в строку для печати.
Итак,
- класс - это инструкция, по которой собирается конкретный объект
- экземпляр - это конкретный объект
- а метод - это функция, присущая конкретному классу объектов.
И это всё?
Нет. =)
Когда мы говорим, что есть 2 экземпляра объектов список:
- [1, 2, 3, 4]
- [1, 2, 3]
то очевидно, что они содержат разные данные.
Т.е. внутри объектов помимо методов есть данные, и хранятся они в атрибутах.
Примером атрибута может быть shape для numpy:
Мы просто создали вектор, передав в него данные. И при этом вычислился его размер.
Логично, для больших матриц в векторов проще хранить внутри 1 переменную с натуральным числом, чем каждый раз проходить по всем данным и тратить время для вычислений.
Давайте попрактикуемся
Создадим пустой класс, который ничего не делает и ничего не хранит:Теперь создадим 1й метод у нашего класса.
Для этого создадим функцию, внутри метода. Она должна принимать по крайней мере 1 аргумент - self - это экземпляр того объекта, методов которого функция будет.
Если мы хотим передавать в метод какие-то параметры, то просто зададим их после self, как и обычные параметры функции:
Это всё здорово, но пока особой разницы между методом и функцией нет.
Можно конечно рассматривать класс как некий контейнер для централизованного хранения функций.
Но обычно всё же класс подразумевает ещё и хранение данных в атрибутах.
Синтаксис для атрибутов аналогичен синтаксису методов (только после атрибута не надо ставить круглые скобки):
- obj.method()
- obj.atr
Давайте создадим атрибут у экземпляра объекта:
Вполне очевидно, что если я создам 2й экземпляр этого же класса, и присвою уже в нём атрибуту с таким же именем какое-то значение, то у разных экземпляров в одноимённом атрибуте будут разные значения:
Так же я могу определить переменную внутри класса и она станет атрибутов всех экземпляров этого класса.
Естественно, это не отменит возможность принудительного переопределения соответствующего атрибута у какого-то экземпляра:
Для экземпляров B и C значение атрибута равно значению по-умолчанию (списку), а для экземпляров E и A переопределено (на число и строку).
Пока пользоваться классами не очень удобно:
- надо определить класс
- создать объект
- обратиться к каждому из атрибутов, записав туда кастомные данные
- обратиться к каждому объекту, передав туда кастомные аргументы
Однако, процесс можно усовершенствовать в помощью конструктора (на самом деле инициализатора, но мы в этой статье не будем углубляться в разницу). Конструктор вызывается автоматически при создании экземпляра.
Для этого служит метод __init__().
Перепишем наш класс и убедимся, что его работа не изменилась:
Важно:
- метод __init__(), как и другие методы класса должен принимать как минимум 1 аргумент self
- к аргументам объекта можно обращаться из метода как аргументам self (например, self.arg)
- Если аргумент создаётся конструктором __init__, то его не нужно описывать в классе как отдельную переменную
Если для конструктора __init__ мы зададим какие-то аргументы кроме self, то их можно будет передать в конструктор при создании экземпляра объекта, и сразу записать в аргументы, если необходимо.
Давайте создадим чуть более осмысленный класс.
Пусть это будет класс "Точка". Точка у нас будет характеризоваться 2 координатами (x, y).
И сразу зададим метод для нашей точки, вычисляющий расстояние от неё до другой точки (для этого воспользуемся теоремой Пифагора):
Здесь метод dist принимает 2 аргумента: экземпляр текущего объекта и ещё одного, между которыми необходимо найти расстояние.
Поскольку метод применяется к текущему экземпляру, то при вызове метода в скобках я указываю только 2ю точку).
Если я хочу передавать в мой метод аргументы привычным образом (как аргументы в скобках по порядку), то мне необходимо указать полный путь до метода. Он начнётся с имени класса:
Обратите внимание, что 2й способ обращения позволяет использовать классы как хранилище функций даже для стандартных типов данных.
Вот пример такого "хранилища":
Заметим, что если мы попытаемся напечатать экземпляр объекта (а не его атрибуты, как раньше), то ничего хорошего не получим:
Мы можем понять экземпляром какого класса объект является и на какую область памяти смотрит указатель, но в работе это довольно бесполезно.
Согласитесь, печатая экземпляр списка мы получаем список - это удобно.
Воспользуемся стандартным методом __str__, который вызывается при его печати с помощью print:
Интересный эффект, который можно заметить - после пересоздания класса ранее созданные объекты не меняют своего поведения.
Дело в том, что класс - это тоже объект.
С точки зрения python мы создали 2 объекта. И на базе 1-ого создали несколько экземпляров.
Поэтому чтобы новые методы добавились у объектов, объекты придётся пересоздать.
Давайте заведём ещё 1 экземпляр объекта с точно такими же координатами, что и первый.
В бытовом понимании 2 такие точки "равны". Но что будет, если мы их сравним?
С точки зрения python это 2 разных объекта, а потому они НЕ равны.
Чтобы это исправить можно написать стандартный метод __eq__:
Замечу, что если мы заведём по новому атрибуту у наших точек (например, цвету), то операция сравнения их учитывать не будет:
Геттеры и Сеттеры
Идея геттеров и сеттеров заключается в том, что передавать в экземпляр класса значения атрибутов явно - довольно опасная идея.Т.к. питон язык с динамический типизацией, то мы можем передать любой тип данных в переменную с одним и тем же именем внутри разных экземпляров (мы так и делали раньше и считали это даже преимуществом)
Но иногда при добавлении атрибута имеет смысл провести валидацию. Или иметь метод который не перезатирает значение, а добавляет (примером такого метода является append для списков).
Добавим в наш класс 2 новых метода:
- сеттер - setColor - проверяет, что передаётся строка и записывает её в атрибут color экземпляра. Если передан другой тип данных, возвращает ошибку.
- геттер - getColor - возвращает значение атрибута color текущего экземпляра
По идее на этом этапе надо переписать все методы нашего класса, чтобы в них использовались геттеры и сеттеры вместо прямых обращений к атрибутам.
=)
Наследование
Идея в том, чтобы хранить в классе только необходимые для него объекты и атрибуты.И структурировать, объединить в иерархию объекты.
Обычно это иллюстрируется на примерах животных: кошечки, собачки, кролики и т.п.
Есть класс животные, он имеет
- атрибуты: число ног, имя, возраст.
- методы: геттеры и сеттеры
Теперь создадим класс кошки. Кошки - тоже животные. Поэтому все атрибуты и методы класса животные им тоже присущи. Но кроме этого, у них могут быть свои:
- атрибуты: имя и т.п.
- методы: "говорение" (мяу) и т.п.
Чтобы не дублировать код, класс кошки наследуется от класса животные.
Наследник получает все методы и атрибуты родителя, а так же может некоторые из них переопределить (например, если мы сделаем класс "птицы" наследником класса "животные", то, вероятно, ограничение на число ног изменится с 4 до 2), а так же задать собственные атрибуты и методы.
Давайте в нашем примере представим, что наш класс точек - это объекты на географической карте. Точка может использоваться для оформления (например, прокладки маршрута из точек).
А новый класс будет представлять из себя географический маркер: банкомат, достопримечательность, организацию или что-то иное.
Новый класс будет наследником обычной точки, но мы чуть расширим конструктор, чтобы иметь больше атрибутов:
Как видим, наш класс получил возможность использовать сеттер родительского класса, да и стандартная функция __str__ тоже наследуется.
Давайте зададим нашему классу новый метод __str__, чтобы при выводе на печать понимать что это не просто точка, именно маркер. И внутри будем использовать метод __str__ родительского класса:
Т.е. вы всегда можете обратиться к родительским методам у экземпляров дочерних классов, если это необходимо.
Давайте создадим ещё пару классов.
1 будет идентичен предыдущим - это будет класс иконок с геттером и сеттером:
Такие классы ещё иногда называются "примесью", дальше станет понятно почему.
А вот 2 будет интереснее. Это будет класс организации. И мы не станем записывать для него ни атрибутов, ни классов. Но унаследуем его от 2 родителей:
Удивительно, но это работает.
При этом:
- наследник получает все методы и атрибуты обоих родителей.
- если у нескольких родителей есть одноимённые методы, то автоматически наследник получит методы того, кто раньше в списке (в нашем случае раньше был гео маркер, поэтому конструктор был унаследован от него, а не от организации).
Наследование от нескольких родителей иногда считается сомнительной практикой, будьте с ним аккуратны.
Переменные класса
Мы уже говорили про атрибуты в качестве хранилища данных (мы даже создавали их внутри класса в самом начале).Есть ещё одно интересное применение для них - переменная класса. Т.е переменные, которые хранятся в классе, а не в экземпляре.
Модифицируем конструктор организации (добавим заодно поддержку url). В классе создадим переменную tag с первоначальным значением равным 0. В конструкторе же запишем в переменную ID создаваемого экземпляра значение из tag, а tag после этого увеличим на 1:
Таким образом мы получили:
- в атрибуте ID каждого объекта хранится его уникальный порядковый номер
- в переменной tag класса хранится число созданных объектов (тут надо быть осторожнее, так как возможно мы захотим уменьшать это число при удалении объекта).
Ну и не забываем, что каждый экземпляр имеет доступ к атрибутам класса помимо собственных, т.е. tag:
Переиспользование методов
Мы уже говорили про наследование методов, мы даже использовали методы родительского класса.Но можно наследовать часть родительского метода (и собирать из нескольких родительских 1 свой). Сократим наш код организации, унаследовав конструкторы гео маркера и иконки:
Мы совместили 2 наследуемых метода и свой код.
И всё это работает.
=)
-
https://www.ibm.com/developerworks/ru/library/l-python_part_6/index.html
- https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BD%D0%B0_Python
- https://pythonworld.ru/osnovy/obektno-orientirovannoe-programmirovanie-obshhee-predstavlenie.html
- https://www.codecademy.com/courses/learn-python/lessons/introduction-to-classes/exercises/why-use-classes
- MIT6.00.1x про ООП: