23 июля 2020

🤹 Введение в объектно-ориентированное программирование (ООП) на Python

Пишу, перевожу и иллюстрирую IT-статьи. На proglib написал 140 материалов. Увлекаюсь Python, вебом и Data Science. Открыт к диалогу – ссылки на соцсети и мессенджеры: https://matyushkin.github.io/links/ Если понравился стиль изложения, упорядоченный список публикаций — https://github.com/matyushkin/lessons
Самое простое введение в объектно-ориентированное программирование (ООП) на Python. Зачем нужны классы, как их использовать, как инициализировать экземпляр, в чём заключается наследование классов.
🤹 Введение в объектно-ориентированное программирование (ООП) на Python

Объектно-ориентированное программирование – это метод структурирования программ путем объединения связанных свойств и методов в отдельные объекты. В этом руководстве мы познакомимся с основами объектно-ориентированного программирования на языке Python. Материал будет полезен абсолютным новичкам в ООП на Python. Чтобы проверить свои знания в Python, вы можете пройти наш тест на знание языка.

Текст публикации представляет собой незначительно сокращенный перевод статьи Дэвида Амоса Object-Oriented Programming (OOP) in Python 3.

Интерактив
Текст адаптирован в виде блокнота Jupyter, который можно запустить в интерактивном виде онлайн в среде Colab. Другие адаптированные таким образом тексты доступны в GitHub-репозитории.

Что собой представляет объектно-ориентированное программирование в Python?

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

Например, объект может представлять человека свойствами «имя», «возраст», «адрес» и методами (поведением) «ходьба», «разговор», «дыхание» и «бег». Или электронное письмо описывается свойствами «список получателей», «тема» и «текст», а также методами «добавление вложений» и «отправка».

Иными словами, объектно-ориентированное программирование – это подход для моделирования вещей, а также отношений между вещами. ООП моделирует сущности реального мира в виде программных объектов, с которыми связаны некоторые данные и которые могут выполнять определенные функции.

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

Определим класс в Python

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

Допустим, вы хотите отслеживать работу сотрудников. Необходимо хранить основную информацию о каждом сотруднике: Ф.И.О., возраст, должность, год начала работы. Один из способов это сделать – представить каждого сотрудника в виде списка:

        kirk = ["James Kirk", 34, "Captain", 2265]
spock = ["Spock", 35, "Science Officer", 2254]
mccoy = ["Leonard McCoy", "Chief Medical Officer", 2266]
    

У этого подхода есть ряд проблем.

Во-первых, ухудшается читаемость кода. Чтобы понять, что kirk[0] ссылается на имя сотрудника, нужно перемотать код к объявлению списка.

Во-вторых, возрастает вероятность ошибки. В приведенном коде в списке mccoy не указан возраст, поэтому mccoy[1] вместо возраста вернет "Chief Medical Officer".

Отличный способ сделать такой тип кода более удобным – использовать классы.

Классы и экземпляры

Итак, для создания пользовательских структур данных используются классы. Классы определяют функции, называемые методами класса. Методы описывают поведение – те действия, которые объект, созданный с помощью класса, может выполнять с данными.

В этом туториале в качестве примера мы создадим класс Dog, который будет хранить информацию о характеристиках собак.

Нужно понимать, что класс – это только план того, как что-то должно быть определено. Сам класс не содержит никаких данных. Класс Dog указывает, что для описания собаки необходимы кличка и возраст, но он не содержит ни клички, ни возраста какой-либо конкретной собаки.

Если класс является планом, то экземпляр – это объект, который построен по этому плану. Он содержит реальные данные, это настоящая собака. Например, 🐕 Майлз, которому недавно исполнилось четыре года.

Другая аналогия: класс – это бланк анкеты. Экземпляр – анкета, которую заполнили 📝. Подобно тому как люди заполняют одну и ту же форму своей уникальной информацией, так из одного класса может быть создано множество экземпляров. Обычно бланк анкеты сам по себе не нужен, он используется лишь для удобства оформления информации.

Как определить класс

Все определения классов начинаются с ключевого слова class, за которым следует имя класса и двоеточие. Весь следующий после двоеточия код составляет тело класса:

        class Dog:
    pass
    

Здесь тело класса Dog пока состоит из одного оператора – ключевого слова-заполнителя pass. Заполнитель позволяет запустить этот код без вызова исключений.

Примечание
Имена классов Python принято записывать в нотации CamelCase.

Определим свойства, которые должны иметь все объекты Dog. Для простоты будем описывать собак с помощью клички и возраста.

Свойства, которые должны иметь все объекты класса Dog, определяются в специальном методе с именем __init__(). Каждый раз, когда создается новый объект Dog, __init __() присваивает свойствам объекта значения. То есть __init__() инициализирует каждый новый экземпляр класса.

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

Обновим класс Dog с помощью метода __init__(), который создает атрибуты name и age:

        class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    

В теле __init__() две инструкции, задействующие переменную self:

  • self.name = name создает атрибут с именем name и присваивает ему значение параметра name.
  • self.age = age создает атрибут age и присваивает ему значение параметра age.

Атрибуты, созданные в __init__() называются атрибутами экземпляра. Значение атрибута экземпляра зависит от конкретного экземпляра класса. Все объекты Dog имеют имя и возраст, но значения атрибутов name и age будут различаться в зависимости от экземпляра Dog.

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

        class Dog:
    # Атрибут класса
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age
    

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

Теперь, когда у нас есть класс Dog, создадим нескольких собак! 🐶

Создание экземпляра класса в Python

Временно воспользуемся простейшим описанием класса, с которого мы начали:

        class Dog:
    pass
    

Создание нового экземпляра класса похоже на вызов функции:

        >>> Dog()
<__main__.Dog at 0x7f6854738150>
    

В памяти компьютера по указанному после at адресу был создан новый объект типа __main__.Dog.

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

        >>> Dog()
<__main__.Dog at 0x7f6854625cd0>
    
        >>> a = Dog()
>>> b = Dog()
>>> a == b
False
    

Хотя a и b являются экземплярами класса Dog, они представляют собой два разных объекта.

Атрибуты класса и экземпляра

Теперь возьмем последнюю рассмотренную нами структуру класса:

        class Dog:
    species = "Canis familiaris"
    def __init__(self, name, age):
        self.name = name
        self.age = age
    

Для создания экземпляров объектов класса необходимо указать кличку и возраст собаки. Если мы этого не сделаем, то Python вызовет исключение TypeError:

        >>> Dog()
[...]

TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'
    

Чтобы передать аргументы, помещаем значения в скобки после имени класса:

        buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)
    

Но ведь в описании класса __init__() перечислены три параметра – почему в этом примере передаются только два аргумента?

При создании экземпляра Python сам передает новый экземпляр в виде параметра self в метод __init__(). Так что нам нужно беспокоиться только об аргументах name и age.

После того как экземпляры созданы, записанные данные доступны в виде атрибутов экземпляра:

        >>> buddy.name
'Buddy'
>>> buddy.age
9
>>> miles.name
'Miles'
>>> miles.age
4
>>> buddy.species
'Canis familiaris'
>>> miles.species
'Canis familiaris'
    

Одним из важных преимуществ использования классов для организации данных является то, что экземпляры гарантированно имеют ожидаемые атрибуты. У всех экземпляров Dog гарантировано есть атрибуты species, name и age.

Значения атрибутов могут изменяться динамически:

        >>> buddy.age = 10
>>> buddy.age
10
>>> miles.species = "Felis silvestris"
>>> miles.species
'Felis silvestris'
    

Экземпляры не зависят друг от друга. Изменение атрибута класса у одного экземпляра не меняет его у остальных экземпляров:

        >>> buddy.species
'Canis familiaris'
    

Методы экземпляра

Методы экземпляра – это определенные внутри класса функции, которые могут вызываться из экземпляра этого класса. Так же, как и у метода __init__(), первым параметром метода экземпляра всегда является self:

        class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Метод экземпляра
    def description(self):
        return f"{self.name} is {self.age} years old"

    # Другой метод экземпляра
    def speak(self, sound):
        return f"{self.name} says {sound}"
    

Мы добавили два метода экземпляра, возвращающих строковые значения. Метод description возвращает строку с описанием собаки, метод speak принимает аргумент sound:

        >>> miles = Dog("Miles", 4)
>>> miles.description()
'Miles is 4 years old'
>>> miles.speak("Woof Woof")
'Miles says Woof Woof'
>>> miles.speak("Bow Wow")
'Miles says Bow Wow'
    

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

К примеру, когда вы создаете объект списка, вы можете использовать для отображения функцию print():

        >>> names = ["Fletcher", "David", "Dan"]
>>> print(names)
['Fletcher', 'David', 'Dan']

    

Посмотрим, что произойдет, когда мы попробуем применить print() к объекту miles:

        >>> print(miles)
<__main__.Dog object at 0x7f6854623690>
    

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

        class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"
    
        >>> miles = Dog("Miles", 4)
>>> print(miles)
Miles is 4 years old
    

Двойные символы подчеркивания в таких методах, как __init__() и __str__() указывают на то, что они имеют предопределенное поведение. Есть множество более сложных методов, которые вы можете использовать для настройки классов в Python, но это тема отдельной публикации.

Наследование от других классов в Python

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

Пример: место для выгула собак

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

Мы можем изменить класс Dog, добавив атрибут breed (англ. порода):

        class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"
    

Смоделируем несколько псов разных пород:

        miles = Dog("Miles", 4, "Jack Russell Terrier")
buddy = Dog("Buddy", 9, "Dachshund")
jack = Dog("Jack", 3, "Bulldog")
jim = Dog("Jim", 5, "Bulldog")
    

У каждой породы собак поведение несколько отличаются. Например, разные породы по-разному лают: одни говорят «гав», другие делают «вуф». Используя только класс Dog, мы были бы должны указывать строку для аргумента sound метода speak() каждый раз, когда вызываем его в экземпляре Dog:

        >>> buddy.speak("Yap")
'Buddy says Yap'
>>> jim.speak("Woof")
'Jim says Woof'
>>> jack.speak("Woof")
'Jack says Woof'
    

Передавать строку в каждый вызов метод speak() неудобно. Более того, строка, соответствующая звуку, который издает экземпляр, в идеале должна определяться атрибутом breed.

Один из вариантов упростить взаимодействие с классом Dog – создать дочерний класс для каждой породы. Это позволит расширить функциональные возможности наследующих дочерних классов. В том числе можно будет указать аргумент по умолчанию для speak.

Создаём дочерние классы

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

        class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"
    

Связь между родительским и дочерним классом определяется тем, что наследуемый класс (Dog) передается в качестве аргумента, принимаемого дочерним классом:

        class JackRussellTerrier(Dog):
    pass

class Dachshund(Dog):
    pass

class Bulldog(Dog):
    pass
    

Дочерние классы действуют так же, как родительский класс:

        miles = JackRussellTerrier("Miles", 4)
buddy = Dachshund("Buddy", 9)
jack = Bulldog("Jack", 3)
jim = Bulldog("Jim", 5)
    

Экземпляры дочерних классов наследуют все атрибуты и методы родительского класса:

        >>> miles.species
'Canis familiaris'
>>> buddy.name
'Buddy'
>>> print(jack)
Jack is 3 years old
>>> jim.speak("Woof")
'Jim says Woof'
    

Чтобы определить, к какому классу принадлежит определенный объект, используйте встроенную функцию type():

        >>> type(miles)
__main__.JackRussellTerrier
    

Чтобы определить, является ли miles экземпляром класса Dog, используем встроенную функцию isinstance():

        >>> isinstance(miles, Dog)
True
    

Объекты miles, buddy, jack и jim являются экземплярами Dog, но miles не является экземпляром Bulldog, а jack не является экземпляром Dachshund:

        >>> isinstance(miles, Bulldog)
False
>>> isinstance(jack, Dachshund)
False

    

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

Теперь дадим нашим собакам немного полаять.

Расширяем функциональность родительского класса

Что мы хотим сделать: переопределить в дочерних классах пород метод speak(). Чтобы переопределить метод, определенный в родительском классе, достаточно создать метод с тем же названием в дочернем классе:

        class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return f"{self.name} says {sound}"
    

Мы переопределили метод speak, добавив для породы JackRussellTerrier значение по умолчанию.

        >>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'
    

Мы по-прежнему можем передать какой-то иной звук:

        >>> miles.speak("Grrr")
'Miles says Grrr'
    

Изменения в родительском классе автоматически распространяются на дочерние классы. Если только изменяемый атрибут или метод не был переопределен в дочернем классе.

        class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} barks {sound}"
    
        >>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'
    

Иногда бывает необходимо учесть и поведение родительского класса, и дочернего, например, вызвать аналогичный метод родительского класса, но модифицировать его поведение. Для вызова методов родительского класса есть специальная функция super:

        class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return super().speak(sound)
    
        >>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles barks Arf'
    

Здесь при вызове 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 проектов в портфолио.

Источники

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ