Объектно-ориентированное программирование – это метод структурирования программ путем объединения связанных свойств и методов в отдельные объекты. В этом руководстве мы познакомимся с основами объектно-ориентированного программирования на языке Python. Материал будет полезен абсолютным новичкам в ООП на Python. Чтобы проверить свои знания в Python, вы можете пройти наш тест на знание языка.
Текст публикации представляет собой незначительно сокращенный перевод статьи Дэвида Амоса Object-Oriented Programming (OOP) in Python 3.
Что собой представляет объектно-ориентированное программирование в Python?
Объектно-ориентированное программирование (ООП) – это парадигма программирования, которая предоставляет средства структурирования программ таким образом, чтобы их свойства и поведение были объединены в отдельные объекты.
Например, объект может представлять человека свойствами «имя», «возраст», «адрес» и методами (поведением) «ходьба», «разговор», «дыхание» и «бег». Или электронное письмо описывается свойствами «список получателей», «тема» и «текст», а также методами «добавление вложений» и «отправка».
Иными словами, объектно-ориентированное программирование – это подход для моделирования вещей, а также отношений между вещами. ООП моделирует сущности реального мира в виде программных объектов, с которыми связаны некоторые данные и которые могут выполнять определенные функции.
Другой распространенной парадигмой программирования является процедурное программирование, которое структурирует программу подобно рецепту. Такая программа предоставляет набор шагов в виде функций и блоков кода, которые последовательно выполняются для выполнения задачи.
Определим класс в Python
Примитивные структуры данных, такие как числа, строки и списки, предназначены для представления простых фрагментов информации: стоимость яблока, название стихотворения, список любимых цветов. Но бывает, что информация имеет более сложную структуру.
Допустим, вы хотите отслеживать работу сотрудников. Необходимо хранить основную информацию о каждом сотруднике: Ф.И.О., возраст, должность, год начала работы. Один из способов это сделать – представить каждого сотрудника в виде списка:
У этого подхода есть ряд проблем.
Во-первых, ухудшается читаемость кода. Чтобы понять, что kirk[0]
ссылается на имя сотрудника, нужно перемотать код к объявлению списка.
Во-вторых, возрастает вероятность ошибки. В приведенном коде в списке mccoy
не указан возраст, поэтому mccoy[1]
вместо возраста вернет "Chief Medical Officer"
.
Отличный способ сделать такой тип кода более удобным – использовать классы.
Классы и экземпляры
Итак, для создания пользовательских структур данных используются классы. Классы определяют функции, называемые методами класса. Методы описывают поведение – те действия, которые объект, созданный с помощью класса, может выполнять с данными.
В этом туториале в качестве примера мы создадим класс Dog
, который будет хранить информацию о характеристиках собак.
Нужно понимать, что класс – это только план того, как что-то должно быть определено. Сам класс не содержит никаких данных. Класс Dog
указывает, что для описания собаки необходимы кличка и возраст, но он не содержит ни клички, ни возраста какой-либо конкретной собаки.
Если класс является планом, то экземпляр – это объект, который построен по этому плану. Он содержит реальные данные, это настоящая собака. Например, 🐕 Майлз, которому недавно исполнилось четыре года.
Другая аналогия: класс – это бланк анкеты. Экземпляр – анкета, которую заполнили 📝. Подобно тому как люди заполняют одну и ту же форму своей уникальной информацией, так из одного класса может быть создано множество экземпляров. Обычно бланк анкеты сам по себе не нужен, он используется лишь для удобства оформления информации.
Как определить класс
Все определения классов начинаются с ключевого слова class
, за которым следует имя класса и двоеточие. Весь следующий после двоеточия код составляет тело класса:
Здесь тело класса Dog
пока состоит из одного оператора – ключевого слова-заполнителя pass
. Заполнитель позволяет запустить этот код без вызова исключений.
CamelCase
.Определим свойства, которые должны иметь все объекты Dog
. Для простоты будем описывать собак с помощью клички и возраста.
Свойства, которые должны иметь все объекты класса Dog
, определяются в специальном методе с именем __init__()
. Каждый раз, когда создается новый объект Dog
, __init __()
присваивает свойствам объекта значения. То есть __init__()
инициализирует каждый новый экземпляр класса.
Методу __init__()
можно передать любое количество параметров, но первым параметром всегда является автоматически создаваемая переменная с именем self
. Переменная self
ссылается на только что созданный экземпляр класса, за счет чего метод __init__()
сразу может определить новые атрибуты.
Обновим класс Dog
с помощью метода __init__()
, который создает атрибуты name
и age
:
В теле __init__()
две инструкции, задействующие переменную self
:
self.name = name
создает атрибут с именемname
и присваивает ему значение параметраname
.self.age
=age
создает атрибутage
и присваивает ему значение параметраage
.
Атрибуты, созданные в __init__()
называются атрибутами экземпляра. Значение атрибута экземпляра зависит от конкретного экземпляра класса. Все объекты Dog
имеют имя и возраст, но значения атрибутов name
и age
будут различаться в зависимости от экземпляра Dog.
С другой стороны, можно создать атрибуты класса – атрибуты, которые имеют одинаковое значение для всех экземпляров класса. Вы можете определить атрибут класса, присвоив значение имени переменной вне __init__()
:
Атрибуты класса определяются после имени класса. Им всегда должно быть присвоено начальное значение. Используйте атрибуты класса для определения свойств, которые должны иметь одинаковое значение для каждого экземпляра класса, а атрибуты экземпляров – для тех данных, которые отличают один экземпляр от другого.
Теперь, когда у нас есть класс Dog
, создадим нескольких собак! 🐶
Создание экземпляра класса в Python
Временно воспользуемся простейшим описанием класса, с которого мы начали:
Создание нового экземпляра класса похоже на вызов функции:
В памяти компьютера по указанному после at
адресу был создан новый объект типа __main__.Dog
.
Важно, что следующий экземпляр Dog
будет создан уже по другому адресу. Это совершенно новый экземпляр и он полностью уникален:
Хотя a
и b
являются экземплярами класса Dog, они представляют собой два разных объекта.
Атрибуты класса и экземпляра
Теперь возьмем последнюю рассмотренную нами структуру класса:
Для создания экземпляров объектов класса необходимо указать кличку и возраст собаки. Если мы этого не сделаем, то Python вызовет исключение TypeError
:
Чтобы передать аргументы, помещаем значения в скобки после имени класса:
Но ведь в описании класса __init__()
перечислены три параметра – почему в этом примере передаются только два аргумента?
При создании экземпляра Python сам передает новый экземпляр в виде параметра self
в метод __init__()
. Так что нам нужно беспокоиться только об аргументах name
и age
.
После того как экземпляры созданы, записанные данные доступны в виде атрибутов экземпляра:
Одним из важных преимуществ использования классов для организации данных является то, что экземпляры гарантированно имеют ожидаемые атрибуты. У всех экземпляров Dog гарантировано есть атрибуты species
, name
и age
.
Значения атрибутов могут изменяться динамически:
Экземпляры не зависят друг от друга. Изменение атрибута класса у одного экземпляра не меняет его у остальных экземпляров:
Методы экземпляра
Методы экземпляра – это определенные внутри класса функции, которые могут вызываться из экземпляра этого класса. Так же, как и у метода __init__()
, первым параметром метода экземпляра всегда является self
:
Мы добавили два метода экземпляра, возвращающих строковые значения. Метод description
возвращает строку с описанием собаки, метод speak
принимает аргумент sound
:
В приведенном примере description()
возвращает строку, содержащую информацию об экземпляре. При написании собственных классов такие методы, описывающие экземпляры, и правда полезны. Однако description()
– не самый элегантный способ это сделать.
К примеру, когда вы создаете объект списка, вы можете использовать для отображения функцию print()
:
Посмотрим, что произойдет, когда мы попробуем применить print()
к объекту miles
:
В большинстве практических приложений информация о расположении объекта в памяти не очень полезна. Поведение объекта при взаимодействии с функцией print()
можно изменить, определив специальный метод __str__()
:
Двойные символы подчеркивания в таких методах, как __init__()
и __str__()
указывают на то, что они имеют предопределенное поведение. Есть множество более сложных методов, которые вы можете использовать для настройки классов в Python, но это тема отдельной публикации.
Наследование от других классов в Python
Наследование – это процесс, при котором один класс принимает атрибуты и методы другого. Вновь созданные классы называются дочерними классами, а классы, от которых происходят дочерние классы, называются родительскими. Дочерние классы могут переопределять или расширять атрибуты и методы родительских классов.
Пример: место для выгула собак
Представьте, что вы в парке, где разрешено гулять с собаками. В парке много собак разных пород, и все они ведут себя по-разному. Предположим, что вы хотите смоделировать парк собак с классами Python. Класс Dog
, который мы написали в предыдущем разделе, может различать собак по имени и возрасту, но не по породе.
Мы можем изменить класс Dog
, добавив атрибут breed
(англ. порода):
Смоделируем несколько псов разных пород:
У каждой породы собак поведение несколько отличаются. Например, разные породы по-разному лают: одни говорят «гав», другие делают «вуф». Используя только класс Dog
, мы были бы должны указывать строку для аргумента sound метода speak()
каждый раз, когда вызываем его в экземпляре Dog
:
Передавать строку в каждый вызов метод speak()
неудобно. Более того, строка, соответствующая звуку, который издает экземпляр, в идеале должна определяться атрибутом breed
.
Один из вариантов упростить взаимодействие с классом Dog
– создать дочерний класс для каждой породы. Это позволит расширить функциональные возможности наследующих дочерних классов. В том числе можно будет указать аргумент по умолчанию для speak
.
Создаём дочерние классы
Создадим дочерние классы для каждой из перечисленных пород. Так как порода теперь будет определяться дочерним классом, её нет смысла указывать в родительском классе:
Связь между родительским и дочерним классом определяется тем, что наследуемый класс (Dog
) передается в качестве аргумента, принимаемого дочерним классом:
Дочерние классы действуют так же, как родительский класс:
Экземпляры дочерних классов наследуют все атрибуты и методы родительского класса:
Чтобы определить, к какому классу принадлежит определенный объект, используйте встроенную функцию type()
:
Чтобы определить, является ли miles
экземпляром класса Dog
, используем встроенную функцию isinstance()
:
Объекты miles
, buddy
, jack
и jim
являются экземплярами Dog, но miles
не является экземпляром Bulldog
, а jack
не является экземпляром Dachshund
:
Все объекты дочернего класса являются экземплярами родительского класса, но не других дочерних классов.
Теперь дадим нашим собакам немного полаять.
Расширяем функциональность родительского класса
Что мы хотим сделать: переопределить в дочерних классах пород метод speak()
. Чтобы переопределить метод, определенный в родительском классе, достаточно создать метод с тем же названием в дочернем классе:
Мы переопределили метод speak
, добавив для породы JackRussellTerrier
значение по умолчанию.
Мы по-прежнему можем передать какой-то иной звук:
Изменения в родительском классе автоматически распространяются на дочерние классы. Если только изменяемый атрибут или метод не был переопределен в дочернем классе.
Иногда бывает необходимо учесть и поведение родительского класса, и дочернего, например, вызвать аналогичный метод родительского класса, но модифицировать его поведение. Для вызова методов родительского класса есть специальная функция super
:
Здесь при вызове super().speak(sound)
внутри класса JackRussellTerrier
, Python ищет родительский класс Dog
(на это указывает функция super()
), и вызывает его метод speak()
с переданной переменной sound
. Именно поэтому выводится глагол barks
, а не says
, но с нужным нам звуком Arf
, который определен в дочернем классе.
JackRussellTerrier
имеет единственный родительский классDog
. В реальных примерах иерархия классов может быть довольно сложной.Функцияsuper()
делает гораздо больше, чем просто ищет в родительском классе метод или атрибут. В поисках искомого метода или атрибута функция проходит по всей иерархии классов. Поэтому без должной осторожности использованиеsuper()
может привести к неожиданным результатам.Заключение
Итак, в этом руководстве мы разобрали базовые понятия объектно-ориентированного программирования (ООП) в Python. Мы узнали:
- в чём отличия классов и экземпляров;
- как определить класс;
- как инициализировать экземпляр класса;
- как определить методы класса и методы экземпляра;
- как одни классы наследуются от других.
Как научиться программировать на Python максимально быстро и качественно?
В условиях повышенной конкуренции среди джунов, пойти учиться на курсы с преподавателями — самый прагматичный вариант, который позволит быстро и качественно освоить базовые навыки программирования и положить 5 проектов в портфолио. Преподаватель прокомментирует домашние задания, поделится полезными советами, когда надо подбодрит или даст «волшебного» пинка.
На курсе «Основы программирования на Python» с преподавателем вы научитесь:
- работать в двух интегрированных средах разработки — PyCharm и Jupyter Notebook;
- парсить веб-страницы;
- создавать ботов для Telegram и Instagram;
- работать с данными для различных материалов и дальнейшего анализа;
- тестировать код.
Плюс положите 5 проектов в портфолио.
Комментарии