Объектно-ориентированное программирование в Java и Python: сходства и отличия
Привет, Хабр! Представляю вашему вниманию перевод статьи “Object-Oriented Programming in Python vs Java” автора Джона Финчера.
Реализация объектно-ориентированного программирования (ООП) в языках Java и Python отличается. Принцип работы с объектами, типами переменных и прочими языковыми возможностями может вызвать затруднение при переходе с одного языка на другой. В данной статье, которая может быть полезной как для Java-программистов, желающих освоить Python, так и для Python-программистов, имеющих цель лучше узнать Java, приводятся основные сходства и отличия этих языков, применительно к ООП.
Подробнее – под катом.
Примеры классов в Python и Java
Для начала давайте реализуем простейший класс в Python и Java, чтобы проиллюстрировать некоторые отличия в этих языках, и будем постепенно вносить в этот класс изменения.
Представим, что у нас есть следующее определение класса Car в Java:
Имя исходного Java-файла должно соответствовать имени хранящегося в нем класса, поэтому мы обязаны назвать файл Car.java. Каждый Java-файл может содержать только один публичный класс.
Такой же класс в Python будет выглядеть так:
В Python вы можете объявить класс где угодно и когда угодно. Сохраним этот файл как car.py.
Используя эти классы как основу, продолжим исследование основных компонентов классов и объектов.
Атрибуты объекта
Во всех объектно-ориентированных языках данные об объекте где-то хранятся. И в Python, и в Java эти данные хранятся в атрибутах, которые являются переменными, связанными с конкретными объектами.
Одним из наиболее значительных отличий между Python и Java является то, как они определяют атрибуты класса и объекта и как эти языки управляют ими. Некоторые из этих различий вызваны ограничениями, налагаемыми языками, в то время как другие связаны с более эффективной практикой.
Объявление и инициализация
В Java мы объявляем атрибуты (с указанием их типа) внутри класса, но за пределами всех методов. Перед тем, как использовать атрибуты класса, мы должны их определить:
В Python же мы объявляем и определяем атрибуты внутри метода класса init(), который является аналогом конструктора в Java:
Указывая перед именем переменных ключевое слово self, мы говорим Python-у, что это атрибуты. Каждый экземпляр класса получает свою копию. Все переменные в Python не типизированы (loosely typed), и атрибуты не являются исключением.
Переменные можно создать и за пределами метода init(), но это не будет лучшим решением и может привести к труднообнаруживаемым багам. Например, можно добавить объекту Car новый атрибут wheels следующим образом:
Однако, если мы забудем указать в 6-й строке выражение my_car.wheels = 5, то получим ошибку:
В Python если объявить переменную за пределами метода, то она будет рассматриваться как переменная класса. Давайте изменим класс Car:
Теперь изменится использование переменной wheels. Вместо обращения к ней через объект, мы обращаемся к ней, используя имя класса:
Примечание: в Python обращение к переменной класса происходит по следующему синтаксису:
- Имя файла, содержащего класс (без расширения .py)
- Точка
- Имя класса
- Точка
- Имя переменной
Поскольку мы сохранили класс Car в файле car.py, мы обращаемся к переменной класса wheels в 6-й строчке таким образом: car.Car.wheels.
Работая с переменной wheels, необходимо быть обратить внимание на то, что изменение значения переменной экземпляра класса my_car.wheels не ведет к изменению переменной класса car.Car.wheels:
На 2-й и 3-й строчках мы определили два объекта Car: my_car и my_other_car.
Сначала свойство wheels у обоих объектов равно нулю. На 16-й строке мы установили переменную класса: car.Car.wheels = 4, у обоих объектов теперь по 4 колеса. Однако, затем когда на 24-й строке мы меняем свойство объекта my_car.wheels = 5, свойство второго объекта остается нетронутым.
Это означает, что теперь у нас две различные копии атрибута wheels:
- Переменная класса, которая применяется ко всем объектам Car
- Конкретная переменная экземпляра класса, которая применяется только к объекту my_car.
Из-за этого можно случайно сослаться не на тот экземпляр и сделать малозаметную ошибку.
В Java эквивалентом атрибута класса является статичный (static) атрибут:
Обычно мы обращаемся к статичным переменным в Java через имя класса. Можно обратиться к ним и через экземпляр класса, как в Python, но это не будет лучшим решением.
Наш Java-класс начинает удлиняться. Одной из причин, почему Java «многословнее» Python-а, является понятие публичных (public) и приватных (private) методов и атрибутов.
Публичные и приватные
Java управляет доступом к методам и атрибутам, различая публичные и приватные данные.
В Java ожидается, что атрибуты будут объявлены как приватные (или защищенные — protected, если нужно обеспечить к ним доступ потомкам класса). Таким образом мы ограничиваем доступ к ним извне. Чтобы предоставить доступ к приватным атрибутам, мы объявляем публичные методы, которые устанавливают или получают эти данные (подробнее об этом – чуть позже).
Вспомним, что в нашем Java-коде переменная color была объявлена приватной. Следовательно, нижеприведенный код не скомпилируется:
Если не указать уровень доступа к атрибутам, то по умолчанию он будет установлен как package protected, что ограничивает доступ к классам в пределах пакета. Если же мы хотим, что вышеуказанный код заработал, то придется сделать атрибут публичным.
Однако, в Java не приветствуется объявление атрибутов публичными. Рекомендуется объявлять их приватными, а затем использовать публичные методы, наподобие getColor() и getModel(), как и было указано в тексте кода выше.
В противоположность, в Python отсутствуют понятия публичных и приватных данных. В Python всё – публичное. Этот питоновский код сработает на ура:
Вместо приватных переменных в Python имеется понятие непубличных (non-public) переменных экземпляра класса. Все переменные, названия которых начинаются с одинарного подчеркивания, считаются непубличными. Это соглашение об именах нисколько не мешает нам обратиться к переменной напрямую.
Добавим следующую строку в наш питоновский класс Car:
Мы можем получить доступ к переменной _cupholders напрямую:
Python позволяет получить доступ к такой переменной, правда, некоторые среды разработки вроде VS Code выдадут предупреждение:

Кроме этого, в Python для того, чтобы скрыть атрибут, используется двойное подчеркивание в начале названия переменной. Когда Python видит такую переменную, он автоматически меняет ее название, чтобы затруднить к ней прямой доступ. Однако, этот механизм всё равно не мешает нам обратиться к ней. Продемонстрируем это следующим примером:
Теперь если мы обратимся к переменной __cupholders, мы получим ошибку:
Так почему же атрибут __cupholders не существует?
Дело вот в чем. Когда Python видит атрибут с двойным подчеркиванием в самом начале, он меняет его, добавляя в начало имя класса с подчеркиванием. Для того чтобы обратиться к атрибуту напрямую, необходимо также изменить имя:
Теперь возникает вопрос: если атрибут Java-класса объявлен приватным и атрибуту Python-класса предшествует в имени двойное подчеркивание, то как достучаться до этих данных?
Управление доступом
В Java мы получаем доступ к приватным атрибутам при помощи сеттеров (setters) и геттеров (getters). Для того чтобы пользователь перекрасил-таки свою машину, добавим следующий кусок кода в Java-класс:
Поскольку методы getColor() и setColor() – публичные, то любой пользователь может вызвать их и получить / изменить цвет машины. Использование приватных атрибутов, к которым мы получаем доступ публичными геттерами и сеттерами, — одна из причин большей «многословности» Java в сравнении с Python.
Как было показано выше, в Python мы можем получить доступ к атрибутам напрямую. Поскольку всё – публичное, мы может достучаться к чему угодно, когда угодно и откуда угодно. Мы можем получать и устанавливать значения атрибутов напрямую, обращаясь по их имени. В Python мы можем даже удалять атрибуты, что немыслимо в Java:
Однако бывает и так, что мы хотим контролировать доступ к атрибутам. В таком случае нам на помощь приходят Python-свойства (properties).
В Python свойства обеспечивают управляемый доступ к атрибутам класса при помощи декораторов (decorators). Используя свойства, мы объявляем функции в питоновских классах подобно геттерам и сеттерам в Java (бонусом идет удаление атрибутов).
Работу свойств можно увидеть на следующем примере класса Car:
В данном примере мы расширяем понятие класса Car, включая электромобили. В строке 6 объявляется атрибут _voltage, чтобы хранить в нем напряжение батареи.
В строках 9 и 10 для контролируемого доступа мы создаем функцию voltage() и возвращаем значение приватной переменной. Используя декоратор @property, мы превращаем его в геттер, к которому теперь любой пользователь получает доступ.
В строках 13-15 мы определяем функцию, так же носящую название voltage(). Однако, мы ее декорируем по-другому: voltage.setter. Наконец, в строках 18-20 мы декорируем функцию voltage() при помощи voltage.deleter и можем при необходимости удалить атрибут _voltage.
Декорируемые функции носят одинаковые имена, указывая на то, что они управляют доступом к одному и тому же атрибуту. Эти имена функций также становятся именами атрибутов, используемых для получения их значений. Вот как это работает:
Обратите внимание, что мы используем voltage, а не _ voltage. Так мы указываем Python-у на то, что следует применять свойства, которые только что определили:
- Когда в 4-й строке выводим значение my_car.voltage, Python вызывает функцию voltage(), декорированную @property.
- Когда в 7-й строке присваиваем значение my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.setter.
- Когда в 13-й строке удаляем my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.deleter.
Вышеприведенные декораторы дают нам возможность контролировать доступ к атрибутам без использования различных методов. Можно даже сделать атрибут свойством только для чтения (read-only), убрав декорированные функции @.setter и @.deleter.
self и this
В Java класс ссылается сам на себя, используя ключевое слово this:
this подразумевается в Java-коде. Его в принципе даже необязательно писать, кроме случаев, когда имена переменных совпадают.
Сеттер можно написать и так:
Поскольку в классе Car есть атрибут под названием color и в области видимости нет больше переменных с таким именем, ссылка на это имя срабатывает. Мы использовали ключевое слово this в первом примере для того, чтобы различать атрибут и параметр с одинаковым именем color.
В Python ключевое слово self служит аналогичной цели: обращение к членам-атрибутам, но в отличие от Java, оно обязательно:
Python требует написания self в обязательном порядке. Каждый self либо создает, либо обращается к атрибуту. Если мы пропустим его, то Python просто создаст локальную переменную вместо атрибута.
Отличие в том, как мы используем self и this в Python и Java, происходит из-за основных различий между двумя языками и от того, как они именуют переменные и атрибуты.
Методы и функции
Разница между рассматриваемыми языками заключается в том, что в Python есть функции, а в Java их нет.
В Python следующий код отработает без проблем (и используется повсеместно):
Мы можем вызвать say_hi() из любого места видимости. Эта функция не содержит ссылки на self, что означает, что это глобальная функция, а не функция класса. Она не сможет изменять или сохранять какие-нибудь данные какого-либо класса, но может использовать локальные и глобальные переменные.
В противоположность, каждая написанная нами строчка на Java принадлежит какому-нибудь классу. Функции не существует за пределами класса, и по определению все Java-функции — это методы. На Java ближе всего к чистой функции находится статичный метод:
Utils. SayHi() вызывается из любого места без предварительного создания экземпляра класса Utils. Поскольку мы вызываем SayHi() без создания объекта, ссылки this не существует. Однако, это всё равно не функция в том смысле, в котором является say_hi() в Python.
Наследование и полиморфизм
Наследование и полиморфизм – две фундаментальные концепции в ООП. Благодаря первому, объекты получают (другими словами, наследуют) атрибуты и функциональные возможности других объектов, создавая иерархию от более общих объектов к более конкретным. Например, и класс Car (машина), и класс Boat (лодка) являются конкретными типами класса Vehicle (транспортное средство). Оба объекта наследуют поведение одного родительского объекта или множества родительских объектов. В этом случае их называют дочерними объектами.
Полиморфизм, в свою очередь, — это возможность работы с разными объектами с помощью одной и той же функции или метода.
Обе эти фундаментальные ООП-концепции реализованы в Java и Python совершенно по-разному.
Наследование
Python поддерживает множественное наследование, то есть создание класса более чем от одного родителя.
Чтобы продемонстрировать это, разделим класс Car на две категории: одну – для транспортных средств и одну – для машин, использующих электричество:
В классе Vehicle определены атрибуты color и model. В классе Device имеется атрибут _voltage. Класс Car происходит от этих двух классов, и атрибуты color, model и _voltage теперь являются частью нового класса.
В методе init() класса Car вызываются методы init() обоих родительских классов, чтобы все данные проинициализировались должным образом. После этого мы можем добавить классу Car любую желаемую функциональность. В данном примере мы добавим атрибут year, а также геттер и сеттер для _voltage.
Функциональность нового класса Car осталась прежней. Мы можем создавать и использовать объекты класса, как это делали несколькими примерами ранее:
Язык Java же, в свою очередь, поддерживает только одиночное наследование, что означает, что классы в Java могут наследовать данные и поведение только от одного родительского класса. Зато в Java возможно наследование от множества интерфейсов. Интерфейсы обеспечивают группу связанных методов, которые нужно реализовать, позволяя дочерним классам вести себя сходным образом.
Чтобы увидеть это, разделим Java-класс Car на родительский класс и интерфейс:
Не забываем, что каждый класс и каждый интерфейс в Java должны быть размещены в своем собственном файле.
Как и в вышеприведенном примере с Python, мы создаем новый класс Vehicle для хранения общих данных и функционала, присущих транспортным средствам. Однако для добавления функциональных возможностей Device нам нужно создать интерфейс, определяющий метод получения напряжения (voltage) устройства.
Класс Car создается путем наследования от класса Vehicle с использованием ключевого слова extends и реализации интерфейса Device с использованием ключевого слова implements. В конструкторе класса мы вызываем конструктор родителя при помощи super(). Поскольку родительский класс только один, мы обращаемся к конструктору класса Vehicle. Для реализации интерфейса переопределяем getVoltage() с помощью аннотации Override.
Вместо повторного использования кода из Device, как это делается в Python, Java требует, чтобы мы реализовывали один и тот же функционал в каждом классе, который реализует интерфейс. Интерфейсы всего лишь определяют методы — они не могут определять данные экземпляра класса или детали реализации.
Так почему же это происходит с Java? Причина кроется в типах данных и проверке типов.
Типы данных и полиморфизм
Каждый класс и каждый интерфейс в Java имеет тип. Следовательно, если два Java-объекта реализуют один и тот же интерфейс, считается, что они имеют один и тот же тип по отношению к этому интерфейсу. С помощью этого механизма можно взаимозаменяемо использовать различные классы, в чем и заключается полиморфизм.
Реализуем зарядку устройства для наших Java-объектов при помощи создания метода charge(), который принимает в качестве параметра переменную типа Device. Любой объект, реализующий интерфейс Device, может быть передан методу charge().
Создадим следующий класс в файле под названием Rhino.java:
Теперь создадим файл Main.java с методом charge() и посмотрим, чем отличаются объекты классов Car и Rhino.
Поскольку в классе Rhino не реализован интерфейс Device, его нельзя передать в качестве параметра в charge().
В отличие от статической типизации (в оригинале — strict variable typing, то есть строгая типизация, но Python тоже относится к языкам со строгой типизацией) переменных, принятой в Java, в Python используется концепция утиной типизации, которая в общем виде звучит так: если переменная «ходит как утка и крякает как утка, то это и есть утка» (на самом деле звучит немного иначе: «если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка» – прим. переводчика). Вместо идентификации объектов по типу, Python проверяет их поведение.
Лучше понять утиную типизацию поможет следующий аналогичный пример зарядки устройства на Python:
charge() проверяет существование в объекте атрибута _voltage. Поскольку в классе Device имеется такой атрибут, то и в любом его классе-наследнике (Car и Phone) тоже будет этот атрибут, и, следовательно, этот класс выведет сообщение о зарядке. У классов, которые не унаследовались от Device (как Rhino), не будет этого атрибута, и они не будут заряжаться, что хорошо, поскольку для жизни носорога (rhino) электрическая зарядка смертельно опасна.
Дефолтные методы
Все классы в Java имеют своим предком класс Object, который содержит определенный набор методов и передает их своим потомкам. Потомки могут эти методы либо переопределять, либо использовать по умолчанию. Класс Object включает в себя следующие методы:
По умолчанию equals() сравнивает адреса в памяти текущего объекта с объектом, переданным в качестве параметра, hashCode() вычисляет уникальный идентификатор, который так же использует адрес в памяти текущего объекта. Эти методы активно используются в Java в различных контекстах. Например, коллекциям, которые сортируют объекты на основе их значений, нужны оба этих метода.
toString() возвращает строковое представление объекта. По умолчанию это имя класса и адрес в памяти. Этот метод вызывается автоматически, когда объект передается в качестве параметра в метод, требующий строковый аргумент, например, System.out.println():
Запустим этот код и увидим дефолтное строковое представление объекта car:
Не очень информативно, не правда ли? Давайте усовершенствуем вывод, переопределив метод toString(). Добавим следующий метод в класс Car:
Теперь, запустив предыдущий пример, увидим следующее:
В Python подобный функционал обеспечивается набором так называемых магических методов (dunder — аббревиатура для double underscore). Каждый Python-класс наследует эти методы, и мы можем, переопределив их, изменить их поведение.
В Python для строкового представления объекта имеется два метода: repr() и str(). Однозначное представление объекта возвращается методом repr(), в то время как str() возвращает его в удобочитаемом виде. Это примерно как hashcode() и toString() в Java.
Как и в Java, в Python имеется дефолтная реализация магических методов:
Чтобы улучшить читаемость, переопределим метод str() в Python-классе Car:
Результат будет выглядеть намного приятнее:
Переопределение магического метода дало нам более читаемое представление объекта. Можно также переопределить метод repr(), это полезно для отладки.
Python предоставляет нам гораздо большее количество магических методов, переопределяя которые, можно изменить поведение объекта во время итерации, операций сравнения и сложения или непосредственного вызова объекта.
Перегрузка операторов
Перегрузка операторов в Python означает возможность в классах переопределять различные операторы языка. Магические методы Python позволяют реализовать перегрузку операторов, чего Java не предлагает вообще.
Изменим наш Python-класс Car следующим образом:
Данная таблица показывает связи между этими магическими методами и операторами, которые они представляют:
| Магический метод | Оператор | Смысл |
|---|---|---|
| eq | == | В один ли год выпущены объекты Car? |
| lt | < | Какой из объектов Car более раннего выпуска? |
| add | + | Добавить два объекта Car без особого смысла |
Когда Python видит выражение, содержащее объекты, он вызывает магический метод, соответствующий операторам в выражении.
В нижеуказанном коде используются новые перегруженные арифметические функции над двумя объектами класса Car:
Существует гораздо большее количество операторов, которые можно перегрузить при помощи магии, что позволяет разнообразить поведение объекта так, как это не делают базовые дефолтные методы Java.
Рефлексия
Рефлексия – это изучение объекта или класса внутри самого объекта или класса. И Java, и Python предоставляют способы исследования атрибутов и методов в классе.
Изучение типа объекта
В обоих рассматриваемых языках имеются способы проверить тип объекта. В Python мы используем type()для отображения типа переменной и isinstance () для определения, является ли данная переменная экземпляром или потомком определенного класса:
В Java мы вызываем метод getClass() для определения типа и используем instanceof для проверки на принадлежность классу:
Изучение атрибутов объекта
В Python при помощи dir() мы видим все атрибуты и функции, содержащиеся в объекте (включая магические методы). Чтобы получить конкретные сведения о данном атрибуте или функции, используем getattr():
В Java имеются аналогичные возможности, однако контроль доступа и типобезопасность, заложенные в языке, усложняют дело.
getFields() извлекает список всех общедоступных атрибутов. Однако, поскольку ни один из атрибутов класса Car не является публичным, этот код возвращает пустой массив:
Java рассматривает атрибуты и методы как отдельные сущности, поэтому публичные методы извлекаются при помощи getDeclaredMethods(). Поскольку публичные атрибуты будут иметь соответствующий get-метод, один из способов обнаружить, что класс содержит определенное свойство, может выглядеть таким образом:
1) использовать getDeclaredMethods() для генерации массива всех методов
2) перебрать все эти методы:
- для каждого обнаруженного метода вернуть true, если метод:
-
- начинается со слова get или принимает ноль аргументов;
- и не возвращает void;
- и включает в себя название свойства;
Вот пример на скорую руку:
getProperty() – это точка входа. Вызовем ее с именем атрибута и объекта. Она вернет true, если свойство будет найдено, иначе вернет false.
Вызов методов через рефлексию
И в Java, и в Python имеются механизмы для вызова методов через рефлексию.
В вышеприведенном Java-примере вместо возвращения значения true в случае, если свойство найдено, можно было вызвать метод напрямую. Вспомним, что getDeclaredMethods() возвращает массив объектов типа Method. Объект Method сам содержит метод invoke(), который вызывает Method. В строке 7 вместо возвращения значения true, когда найден метод, можно вернуть method.invoke(object).Эта возможность существует также и в Python. Однако, поскольку Python не делает различий между функциями и атрибутами, нужно специально искать сущности, которые можно вызвать:
Методы Python проще в управлении и вызове, чем в Java. Нижеприведенный код найдет метод объекта str() и вызовет его через рефлексию:
В данном примере проверяется каждый атрибут, возвращаемый функцией dir(). Мы получаем значение атрибута объекта, используя getattr(), и проверяем при помощи callable(), является ли оно вызываемой функцией. Если это так, то можно проверить, является ли его имя str (), и затем вызвать его.
Урок 6. Принципы ООП. Классы, объекты, поля и методы. Уровни доступа.
Поговорим про основные принципы объектно-ориентированного программирования: абстракцию, инкапсуляцию, наследование и полиморфизм. Научимся создавать классы и объекты классов в Python. Рассмотрим, чем отличаются понятия поля, свойства, методы и атрибуты класса. Изучим особенности организации уровней доступа к атрибутам: Public, Protected и Private.

Урок 6.
Принципы ООП. Классы, объекты, поля и методы. Уровни доступа.
- Процедурное программирование
- Объектно-ориентированное программирование (оно же ООП)
По сути любая программа представляет собой совокупность данных и операций по их обработке. Но что важнее, сами данные или операции над ними? В языках, в основе работы которых лежит принцип процедурного программирования ( Basic , C , Pascal , Go ), главным является код для обработки данных. При этом сами данные имеют второстепенное значение.

В чем суть процедурного подхода? Процедурное программирование – это написание функций и их последовательный вызов в некоторой главной( main ) функции.
Для каких проектов подходит процедурное программирование? Идеальные условия для применения данного подхода — простые программы, где весь функционал можно реализовать несколькими десятками процедур/функций. Функции аккуратно вложены друг в друга и легко взаимодействуют посредством передачи данных из одной функции в другую.
Однако проблема в том, что когда мы выходим за пределы этих идеальных условий, выплывают наружу многие недостатки данного подхода:
- В больших проектах приходится создавать огромное количество процедур и функций. В свою очередь, это неизбежно ведет к возникновению множества ошибок и снижает читаемость кода программы.
- Все данные внутри процедуры видны только локально, а значит их нельзя использовать в другом месте. Как следствие, код наполняется дубликатами.
- Высокий порог вхождения — по статистике начинающим данный поход дается сложнее, чем ООП.

- Программа разбивается на объекты. Каждый объект отвечает за собственные данные и их обработку. Как результат — код становится проще и читабельней.
- Уменьшается дупликация кода. Нужен новый объект, содержимое которого на 90% повторяет уже существующий? Давайте создадим новый класс и унаследуем эти 90% функционала от родительского класса!
- Упрощается и ускоряется процесс написания программ. Можно сначала создать высокоуровневую структуру классов и базовый функционал, а уже потом перейти к их подробной реализации.
- В процедурном подходе основой программы является функция. Функции вызывают друг друга и при необходимости передают данные. В программе функции живут отдельно, данные — отдельно.
- Основной недостаток процедурного подхода — сложность создания и поддержки больших программ. Наличие сотен функций в таких проектах очень часто приводит к ошибкам и спагетти-коду.
- В основе объектно-ориентированного программирования лежит понятие объекта. Объект совмещает в себе и функции и данные.
- Основное преимущество ООП перед процедурным программированием — изоляция кода на уровне классов, что позволяет писать более простой и лаконичный код.

- Класс описывает множество объектов, имеющих общую структуру и обладающих одинаковым поведением. Класс — это шаблон кода, по которому создаются объекты. Т. е. сам по себе класс ничего не делает, но с его помощью можно создать объект и уже его использовать в работе.
- Данные внутри класса делятся на свойства и методы. Свойства класса (они же поля или атрибуты) — это характеристики объекта класса.
- Методы класса — это функции, с помощью которых можно оперировать данными класса.
- Объект — это конкретный представитель класса.
- Объект класса и экземпляр класса — это одно и то же.
- Цвет
- Объем двигателя
- Мощность
- Тип коробки передач
- Ехать
- Остановиться
- Заправиться
- Поставить на сигнализацию
- Включить дворники
Что общего будет в объектах? Все объекты создаются по одному шаблону, то есть на выходе обязательно будут машины, никаких велосипедов и мотоциклов. Они будут выкрашены в какой-то цвет, ехать они будут за счет наличия в них двигателя, скорость будет регулироваться с помощью коробки передач. Также объекты данного класса будут обладать одинаковыми методами — все машины этого класса будут ездить, периодически им будет нужна заправка, а от угона они будут защищены установкой сигнализации.

Но в чем разница? Значения свойств будут различаться. Одна машина красная, другая — зеленая. У одной объем двигателя 1968 см3 и коробка-робот, а у другой — 1395 см3 и ездить владельцу придется на механике.
Вывод: Объекты класса на выходе похожие и одновременно разные. Различаются, как правило, свойства. Методы остаются одинаковыми.
- Свойства: Цвет=»Белый», Объем двигателя=»1984 см3″, Мощность=»180 л.с.», Тип коробки передач=»Робот»
- Методы: Ехать, Остановиться, Заправиться, Поставить на сигнализацию, Включить дворники

Принцип 1. Абстракция
- Выделить главные и наиболее значимые свойства предмета.
- Отбросить второстепенные характеристики.
Зачем нужна абстракция? Если мыслить масштабно — то она позволяет бороться со сложностью реального мира. Мы отбрасываем все лишнее, чтобы оно нам не мешало, и концентрируемся только на важных чертах объекта.

Принцип 2. Инкапсуляция
Абстракция утверждает следующее: «Объект может быть рассмотрен с общей точки зрения». А инкапсуляция от себя добавляет: «И это единственная точка зрения, с которой вы вообще можете рассмотреть этот объект.». А если вы внимательно посмотрите на название, то увидите в нем слово «капсула». В этой самой «капсуле» спрятаны данные, которые мы хотим защитить от изменений извне.
- Отсутствует доступ к внутреннему устройству программного компонента.
- Взаимодействие компонента с внешним миром осуществляется посредством интерфейса, который включает публичные методы и поля.
- Инкапсуляция упрощает процесс разработки, т. к. позволяет нам не вникать в тонкости реализации того или иного объекта.
- Повышается надежность программ за счет того, что при внесении изменений в один из компонентов, остальные части программы остаются неизменными.
- Становится более легким обмен компонентами между программами.

Принцип 3. Наследование
- Класс-потомок = Свойства и методы родителя + Собственные свойства и методы.
- Класс-потомок автоматически наследует от родительского класса все поля и методы.
- Класс-потомок может дополняться новыми свойствами.
- Класс-потомок может дополняться новыми методами, а также заменять(переопределять) унаследованные методы. Переопределить родительский метод — это как? Это значит, внутри класса потомка есть метод, который совпадает по названию с методом родительского класса, но функционал у него новый — соответствующий потребностям класса-потомка.
- Дает возможность использовать код повторно. Классы-потомки берут общий функционал у родительского класса.
- Способствует быстрой разработке нового ПО на основе уже существующих открытых классов.
- Наследование позволяет делать процесс написания кода более простым.
МЕТОДЫ
1) Построить (УНАСЛЕДОВАНО)
2) Отремонтировать (УНАСЛЕДОВАНО)
3) Заселить (УНАСЛЕДОВАНО)
4) Снести (УНАСЛЕДОВАНО)
5) Изменить фасад
6) Утеплить
7) Сделать пристройкуСВОЙСТВА
1) Тип фундамента (УНАСЛЕДОВАНО)
2) Материал крыши (УНАСЛЕДОВАНО)
3) Количество окон (УНАСЛЕДОВАНО)
4) Количество дверей (УНАСЛЕДОВАНО)
5) Количество квартир
6) Количество подъездов
7) Наличие коммерческой недвижимостиМЕТОДЫ
1) Построить (УНАСЛЕДОВАНО)
2) Отремонтировать (УНАСЛЕДОВАНО)
3) Заселить (УНАСЛЕДОВАНО)
4) Снести (УНАСЛЕДОВАНО)
5) Выбрать управляющую компанию
6) Организовать собрание жильцов
7) Нанять дворника
Принцип 4. Полиморфизм
Другими словами, полиморфизм позволяет перегружать одноименные методы родительского класса в классах-потомках.
Также для понимания работы этого принципа важным является понятие абстрактного метода:
- В родительском классе(в нашем случае — класс Дом) создают пустой метод(например, метод Построить() ) и делают его абстрактным.
- В классах-потомках создают одноименные методы, но уже с соответствующей реализацией. И это логично, ведь например, процесс постройки Частного и Многоквартирного дома отличается кардинально. К примеру, для строительства Многоквартирного дома необходимо задействовать башенный кран, а Частный дом можно построить и собственными силами. При этом данный процесс все равно остается процессом строительства.
- В итоге получаем метод с одним и тем же именем, который встречается во всех классах. В родительском — имеем только интерфейс, реализация отсутствует. В классах-потомках — имеем и интерфейс и реализацию. Причем в отличие от родительского класса реализация в потомках уже становится обязательной.
- Теперь мы можем увидеть полиморфизм во всей его красе. Даже не зная, с объектом какого из классов-потомков мы работаем, нам достаточно просто вызвать метод Построить(). А уже в момент исполнения программы, когда класс объекта станет известен, будет вызвана необходимая реализация метода Построить().


Как мы видим, для задания класса используется инструкция class , далее следует имя класса Car и двоеточие. После них идет тело класса, которое в нашем случае представлено оператором pass . Данный оператор сам по себе ничего не делает — фактически это просто заглушка.
Чтобы создать объект класса, нужно воспользоваться следующим синтаксисом:

Давайте договоримся, что атрибутом класса/объекта мы будем называть любой элемент класса/объекта (переменную, метод, подкласс), на который мы можем сослаться через символ точки. Т. е. вот так: MyClass.<атрибут> или my_object.<атрибут> .
- Встроенные(служебные) атрибуты
- Пользовательские атрибуты

1. Встроенные атрибуты
Это важно
В теории ООП конструктор класса — это специальный блок инструкций, который вызывается при создании объекта. При работе с питоном может возникнуть мнение, что метод __init__(self) — это и есть конструктор, но это не совсем так. На самом деле, при создании объекта в Python вызывается метод __new__ (cls, *args, **kwargs) и именно он является конструктором класса.Также обратите внимание, что __new__() — это метод класса, поэтому его первый параметр cls — ссылка на текущий класс. В свою очередь, метод __init__() является так называемым инициализатором класса. Именно этот метод первый принимает созданный конструктором объект. Как вы уже, наверное, не раз замечали, метод __init__() часто переопределяется внутри класса самим программистом. Это позволяет со всем удобством задавать параметры будущего объекта при его создании.
2. Пользовательские атрибуты
Это атрибуты, которые непосредственно составляют основной функционал класса. Если служебные атрибуты наследуются от базового класса object , то пользовательские — пишутся программистом во время реализации начинки класса и дальнейшей работы с ним.
Список атрибутов класса / объекта можно получить с помощью команды dir() . Если взять самый простой класс:
- Атрибутами называем совокупность полей и методов класса / объекта.
- Атрибуты делятся на встроенные и пользовательские.
- Все классы в Python имеют общий родительский класс — он называется object .
- Класс object предоставляет всем своим потомкам набор служебных атрибутов (как переменных (например, __dict__ и __doc__ ), так и методов (например, __str__ ) ).
- Многие из служебных атрибутов можно переопределить внутри своего класса.
- Поля и методы, которые описываются программистом в теле класса, являются пользовательскими и добавляются в общий список атрибутов наряду со встроенными атрибутами.

- Статические поля
- Динамические поля

1. Статические поля (они же переменные или свойства класса)
2. Динамические поля (переменные или свойства экземпляра класса)
Что такое self в Python?
Служебное слово self — это ссылка на текущий экземпляр класса. Как правило, эта ссылка передается в качестве первого параметра метода Python:class Apple:
____# Создаем объект с общим количеством яблок 12
____def __init__(self):
________self.whole_amount = 12____# Съедаем часть яблок для текущего объекта
____def eat(self, number):
________self.whole_amount -= numberСтоит обратить внимание, что на самом деле слово self не является зарезервированным. Просто существует некоторое соглашение, по которому первый параметр метода именуется self и передает ссылку на текущий объект, для которого этот метода был вызван. Хотите назвать первый параметр метода по-другому — пожалуйста.
В других языках программирования(например, Java или C++) аналогом этого ключа является служебное слово this .
- Для создания статической переменной достаточно объявления класса, причем данная переменная создается непосредственно в теле класса.
- Динамические переменные создаются только в рамках работы c экземпляром класса.
- Чтоб создать переменную экземпляра, необходимо воспользоваться конструкцией self.<имя_переменной> внутри одного из методов.
- Экземпляр класса сочетает в себе совокупность как статических (уровня класса), так и динамических (уровня самого экземпляра) полей.
- Значения динамических переменных для разных объектов класса могут (и чаще всего так и делают) различаться.

- Методы экземпляра класса (они же обычные методы)
- Статические методы
- Методы класса

1. Методы экземпляра класса (Обычные методы)
2. Статические методы
Статические методы — это обычные функции, которые помещены в класс для удобства и тем самым располагаются в области видимости этого класса. Чаще всего это какой-то вспомогательный код.
Важная особенность заключается в том, что данные методы можно вызывать посредством обращения к имени класса, создавать объект класса при этом не обязательно. Именно поэтому в таких методах не используется self — этому методу не важна информация об объектах класса.
Чтобы создать статический метод в Python, необходимо воспользоваться специальным декоратором — @staticmethod . Выглядит это следующим образом:
3. Методы класса
Методы класса являются чем-то средним между обычными методами (привязаны к объекту) и статическими методами (привязаны только к области видимости). Как легко догадаться из названия, такие методы тесно связаны с классом, в котором они определены.
Обратите внимание, что такие методы могут менять состояние самого класса, что в свою очередь отражается на ВСЕХ экземплярах данного класса. Правда при этом менять конкретный объект класса они не могут (этим занимаются методы экземпляра класса).
Чтобы создать метод класса, необходимо воспользоваться соответствующим декоратором — @classmethod . При этом в качестве первого параметра такого метода передается служебное слово cls , которое в отличие от self является ссылкой на сам класс (а не на объект). Рассмотрим пример:
- Необходимо создать специфичный объект текущего класса
- Нужно реализовать фабричный паттерн — создаём объекты различных унаследованных классов прямо внутри метода

Вам наверняка известно, что в классических языках программирования (таких как C++ и Java) доступ к ресурсам класса реализуется с помощью служебных слов public , private и protected :
- Private. Приватные члены класса недоступны извне — с ними можно работать только внутри класса.
- Public. Публичные методы наоборот — открыты для работы снаружи и, как правило, объявляются публичными сразу по-умолчанию.
- Protected. Доступ к защищенным ресурсам класса возможен только внутри этого класса и также внутри унаследованных от него классов (иными словами, внутри классов-потомков). Больше никто доступа к ним не имеет
- Если переменная/метод начинается с одного нижнего подчеркивания ( _protected_example ), то она/он считается защищенным ( protected ).
- Если переменная/метод начинается с двух нижних подчеркиваний ( __private_example ), то она/он считается приватным ( private ).

Иными словами, это больше вопрос ответственности программиста — он не должен работать с атрибутами, имена которых начинаются с нижнего подчёркивания _ , снаружи класса.
Аналогично, два нижних подчеркивания __ в названии свойства/метода делают его приватным ( private ). Здесь уже интереснее — получить доступ к таким атрибутам напрямую нельзя (но если очень хочется, то все равно можно — об этом чуть ниже):
- Существует три уровня доступа к свойствам/методам класса: public, protected, private
- Физически данный механизм ограничения доступа к атрибутам класса в Python реализован слабо, что от части может противоречить одному из главных принципов ООП — инкапсуляции.
- Однако существует некоторое соглашение, по которому в Python задать уровень доступа к свойству/методу класса можно с помощью добавления к имени одного ( protected ) или двух ( private ) подчеркиваний. Ответственность за соблюдение данного соглашения ложится на плечи программистов.

Как мы уже выяснили выше, механизм наследования позволяет создать новый класс на основе уже существующего. При этом новый класс включает в себя как свойства и методы родительского класса, так и новые (собственные) атрибуты. Эти новые атрибуты и отличают свежесозданный класс от его родителя.
Для того, чтобы в Python создать новый класс с помощью механизма наследования, необходимо воспользоваться следующим синтаксисом:
Теперь давайте рассмотрим пример применения механизма наследования в действии. Перед нами класс Phone (Телефон), у которого есть одно свойство is_on и три метода:
- Инициализатор: __init__()
- Включение: turn_on()
- Звонок: call()
Среди данной совокупности атрибутов нас больше всего интересуют пользовательские свойства и методы: ‘__init__’ , ‘call’ , ‘is_on’ , ‘turn_on’
А теперь предположим, что мы захотели создать новый класс — MobilePhone (Мобильный телефон). Хоть этот класс и новый, но это по-прежнему телефон, а значить — его все так же можно включить и по нему можно позвонить. А раз так, то нам нет смысла реализовывать этот функционал заново, а можно просто унаследовать его от класса Phone . Выглядит это так:
Что такое super?
Как вы могли заметить, в инициализаторе (метод __init__ ) наследуемого класса вызывается метод super() . Что это за метод и зачем он нужен?Главная задача этого метода — дать возможность наследнику обратиться к родительскому классу. В классе родителе Phone есть свой инициализатор, и когда в потомке MobilePhone мы так же создаем инициализатор (а он нам действительно нужен, так как внутри него мы хотим объявить новое свойство) — мы его перегружаем. Иными словами, мы заменяем родительский метод __init__() собственным одноименным методом. Это чревато тем, что родительский метод просто в принципе не будет вызван, и мы потеряем его функционал в классе наследнике. В конкретном случае, потеряем свойство is_on .
- Внутри инициализатора класса-наследника вызвать инициализатор родителя (для этого вызываем метод super().__init__() )
- А затем просто добавить новый функционал
def __init__(self):
____super().__init__()
____self.battery = 0Обратите внимание, что вызывать родительский метод необходимо в первую очередь.
Объект
В реальном мире мы с тобой постоянно взаимодействуем с какими-то объектами: стул, стол, кружка, телефон и т.д.
Как это всё может относиться к программированию?
До сих пор, твои программы представляли собой набор функций, которые вызывают друг друга в определённой последовательности (или просто одна функция). Этот подход называется функциональным – он хорошо себя показывает если нужно написать программу, которая выполняет только одну задачу.
Однако, если твоя программа делает сразу много задач, то тебе необходимо использовать ООП. С помощью этого подхода, ты сможешь представить свою программу как набор объектов, каждый из которых будет ответствененн только за свою область задач.
Вот пример простой программы “Список рецептов”, в которой можно только создавать, изменять и удалять рецепты:

Всё что я сделал – это перенёс функции программы по разным объектам.
Чтобы добавить ещё больше контекста – вот примеры задач, которые я мог бы решать в этой программе, как программист:
- Нужно чтобы теперь файл базы данных лежал в другой папке:
- Залезу в код объекта “База Данных”:
- Поменяю путь до файла
- Залезу в код объекта “Пользовательский Интерфейс”:
- Поменяю свойство размера кнопок
- Залезу в код объекта “Пользовательский Интерфейс”:
- Добавлю новое численное поле ввода
- Подпишу его “Время приготовления”
- Добавлю дополнительный параметр cooking_duration в dict, который отправляю в объект “Список Рецептов”
- Добавлю логику получения значения cooking_duration из данных от объекта “Пользовательский Интерфейс”
- Изменю запросы на добавление/изменение/удаление в объект “База Данных”, чтобы они учитывали новое значение cooking_duration
В этом случае ООП не убавило работы – мне нужно писать столько же кода, чтобы выполнить задачу. Однако, теперь весь этот код сгруппирован в объектах, и мне намного легче удержать в голове взаимосвязи между тремя объектами, нежели множественные вызовы между десятками функций.
Это я в общих чертах описал чем отличается ООП от функционального программирования, которым мы занимались до этого. Теперь рассмотрим классы.
Класс
Класс – это описание объекта, а объект – это существующий экземпляр класса.

То есть если мы описали какой-то класс, мы не можем его использовать по назначению – нельзя сесть на описание стула, можно сесть только на экземпляр стула.
Вот пример объявления класса, создания и использования объектов в Python:
Скопируй это код себе, и давай разберём его.
Объявление класса
В начале пишем имя класса, по которому мы в дальнейшем будем обращаться при создании объектов:
Затем идёт определение метода __init__ – этот метод также называется конструктором объекта.
Метод – это функция, которая объявлена внутри какого-то класса.
Используется именно такой странное название метода, потому что имя __init__ зарезервированно Python, и метод с таким именем автоматически вызывается при создании объекта.
В круглых скобках после названия как и в функции указываются параметры. В методах есть одна особенность – во всех методах класса первым параметром передаётся self. Именно через self мы будем обращаться к объекту и его полям.
Поле объекта – это переменная, которая хранится внутри этого объекта. У каждого объекта свой набор полей.
В методе __init__ мы создаём и заполняем поля legs, seat и back значениями, которые были переданы в параметрах конструктора.
На этом логика конструктора заканчивается.
Кроме этого, мы добавляем метод take_a_seat, который “симулирует” процесс восседания на стул. В нём просто выводятся значения полей объекта.
И ещё один метод compare_legs_count, который сравнивает количество ног стула с переданным через параметры. Я его добавил, чтобы показать как выглядят методы с параметрами.
Создание объекта
Далее, мы создаём объекты – экземпляры класса Chair.
Для того, чтобы создать объект, надо написать имя класса, и в круглых скобках указать параметры, которые будут переданы в конструктор.
Таким образом в переменную запишется объект.
Окей, а что с ними делать?
Вызов методов класса
Помнишь мы создали методы take_a_seat и compare_legs_count? Так вот, мы можем их вызывать:
Обрати внимание, что в зависимости от объекта, у нас получается разный вывод – мы же при создании объектов передавали разные параметры.
Мы никогда не передаём параметр self, так как Python сам его докидывает – ты же указывай всё остальное.
Обращение к полям класса
Чтобы обратиться к полю класса, надо написать “ОБЪЕКТ.ПОЛЕ”. С нимим можно работать как с обычными переменными, в них нет ничего особенного – просто они лежат в объекте.
Задание на закрепление
Создай класс Calculator:
- В нём должно быть поле “acc”, в которое в конструкторе класса присваивается 0
- В нём должны быть методы:
- add – принимает один параметр, складывает его с “acc” и кладёт результат в “acc”
- sub – принимает один параметр, вычитает его из “acc” и кладёт результат в “acc”
- mul – принимает один параметр, умножает его на “acc” и кладёт результат в acc
- div – принимает один параметр, делит “acc” на него и кладёт результат в acc
- clr – не принимает параметров, сбрасывает значение поля acc в 0
- res – не принимает параметров, возвращает значение поля acc
После того, как создашь класс Calculator, исполни этот код:
Если посыпятся ошибки, то класс создан не по ТЗ 🙂
Заключение
Итого, мы изучили:
- Объектно-ориентированное программирование
- Объекты
- Классы (методы, конструктор, поля)
Теперь первые два пункта в определении ООП понятны:
- Программа представляется в виде набора объектов и связей между ними
- Все объекты являются экземплярами классов
- Классы образуют иерархию наследования
Если что – пиши, я помогу и постараюсь объяснить лучше.
Дальше мы залезем поглубже в принципы ООП – сложные, но полезные понятия.
- Залезу в код объекта “База Данных”: