Рубріки: Теория

Python: объектно-ориентированное программирование (ООП). Практикум

Игорь Грегорченко

Python — это суперпопулярный язык программирования, особенно подходящий для AI&ML (data science) и веб-приложений. Он также является удобным выбором для разработки современных приложений, поскольку предлагает динамическую типизацию и возможности связывания. В этой статье мы познакомимся с практическими аспектами объектно-ориентированного программирования (ООП) на языке Python.



Напомним, что это вторая часть текста (практикум), начало с введением и теорией ищите вот здесь. В прошлой части мы рассмотрели следующие темы:

  • Что такое концепция объектно-ориентированного программирования Python?
  • Что такое класс?
  • Что такое объект?
  • Метод _init_method.
  • Создание классов и объектов в Python.
  • Концепция ООП (OOP) в Python.
  • Наследование.
  • Полиморфизм.
  • Абстракция.
  • Инкапсуляция.

В этой части мы основательно сосредоточимся на закреплении всего этого материала, а еще точнее — на практике и примерах создания/применения классов и объектов. Далее по тексту мы будем активно работать с IDLE IDE, хотя вы можете использовать и любое другое свое IDE, которое понравится вам больше (JetBrains PyCharm или IntelliJ IDEA).

Создание инстанса объекта в Python

Откройте интерактивное окно IDLE IDE и введите там следующее:

>>> class Dog:
...     pass

Это создает новый класс Dog без атрибутов и методов.

Создание нового объекта из класса называется инстанцированием объекта (или созданием инстанса, копии объекта). Вы можете создать новый объект Dog, набрав имя класса, за которым следуют открывающие и закрывающие круглые скобки:

>>> Dog()
<__main__.Dog object at 0x106702d30>

Теперь у вас есть новый объект Dog по адресу 0x106702d30. Эта забавно выглядящая строка букв и цифр является адресом памяти, который указывает, где объект Dog хранится в памяти вашего компьютера. Обратите внимание, что адрес, который вы видите на вашем экране, будет другим.

Теперь создайте второй объект Dog:

>>> Dog()
<__main__.Dog object at 0x0004ccc90>

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

Чтобы увидеть это еще и с другой стороны, напечатайте следующее:

>>> a = Dog()
>>> b = Dog()
>>> a == b
False

В этом коде вы создаете два новых объекта Dog и присваиваете их переменным a и b. Когда вы сравниваете a и b с помощью оператора ==, результатом будет False. Несмотря на то, что a и b являются экземплярами класса Dog, в памяти они представляют два разных объекта.

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

Теперь создайте новый класс Dog с атрибутом класса под названием .species и двумя атрибутами экземпляра под названием .name и .age:

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

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

>>> Dog()
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    Dog()
TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'

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

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

Это создаст два новых экземпляра Dog — один для девятилетней собаки по имени Бадди и один для четырехлетней собаки по имени Майлз.

Метод .__init__() класса Dog имеет три параметра, почему же в примере ему передаются только два аргумента?

Когда вы инстанцируете объект Dog, Python создает новый экземпляр и автоматически передает его в первый параметр метода .__init__(). Это, по сути, удаляет (заменяет) дефолтный параметр self, поэтому вам нужно беспокоиться только о параметрах name и age.

После создания экземпляров Dog вы можете получить доступ к их атрибутам с помощью точечной нотации:

>>> buddy.name
'Buddy'
>>> buddy.age
9

>>> miles.name
'Miles'
>>> miles.age
4

Таким же образом можно получить доступ к атрибутам класса:

>>> buddy.species
'Canis familiaris'

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

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

>>> buddy.age = 10
>>> buddy.age
10

>>> miles.species = "Felis silvestris"
>>> miles.species
'Felis silvestris'

В этом примере вы изменяете атрибут .age объекта buddy на 10. Затем вы изменяете атрибут .species объекта miles на «Felis silvestris», который является разновидностью кошки. Это делает Майлза довольно странной собакой, но зато это правильный Python!

Ключевым моментом здесь является то, что пользовательские объекты по умолчанию являются изменяемыми. Объект является изменяемым, если он может быть изменен динамически. Например, списки и словари являются изменяемыми, а строки и кортежи — неизменяемыми.

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

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

Откройте новое окно редактора в IDLE и введите следующий класс Dog:

class Dog:
    species = "Canis familiaris"

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

    # Instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

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

У этого класса Dog есть два метода экземпляра:

  1. .description() возвращает строку, содержащую имя и возраст собаки.
  2. .speak() имеет один параметр под названием sound и возвращает строку, содержащую имя собаки и звук, который она издает.

Сохраните модифицированный класс Dog в файл с именем dog.py и нажмите F5, чтобы запустить программу. Затем откройте интерактивное окно и введите следующий текст, чтобы увидеть методы экземпляра в действии:

>>> 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'

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

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

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

Давайте посмотрим, что произойдет, если вы выведете print() объект miles:

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

Когда вы выводите print(miles), вы получаете загадочное сообщение о том, что miles — это объект Dog по адресу памяти 0x00aeff70. Это сообщение не очень полезно. Вы можете изменить то, что будет напечатано, определив специальный метод экземпляра под названием .__str__().

В окне редактора измените имя метода .description() класса Dog на .__str__():

class Dog:
    # Leave other parts of Dog class as-is

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

Сохраните файл и нажмите F5. Теперь, когда вы печатаете print(miles), вы получаете гораздо более дружественный вывод:

>>> miles = Dog("Miles", 4)
>>> print(miles)
'Miles is 4 years old'

Такие методы, как .__init__() и .__str__(), называются методами Дандера, потому что они начинаются и заканчиваются двойным подчеркиванием. Существует множество Dunder methods, которые можно использовать для настройки классов в Python. Хотя это слишком сложная тема для этого вводного руководства по Python для начинающих, понимание dunder-методов является важной частью освоения объектно-ориентированного программирования на 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

Методы экземпляра, определенные ранее, здесь опущены, поскольку они не важны для данного обсуждения.

Нажмите F5, чтобы сохранить файл. Теперь вы можете смоделировать собачий парк, создав несколько различных собак в интерактивном окне:

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

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

Используя только класс Dog, вы должны предоставлять строку для звукового аргумента функции .speak() каждый раз, когда вызываете ее для экземпляра Dog:

>>> buddy.speak("Yap")
'Buddy says Yap'

>>> jim.speak("Woof")
'Jim says Woof'

>>> jack.speak("Woof")
'Jack says Woof'

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

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

Родительские классы и дочерние классы

Давайте создадим дочерний класс для каждой из трех вышеупомянутых пород: Джек Рассел Терьер, Такса и Бульдог.

Для справки, вот полное определение класса Dog:

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.py, чтобы создать три новых дочерних класса от родительского класса Dog:

class JackRussellTerrier(Dog):
    pass

class Dachshund(Dog):
    pass

class Bulldog(Dog):
    pass

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

>>> 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)
<class '__main__.JackRussellTerrier'>

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

>>> isinstance(miles, Dog)
True

Обратите внимание, что isinstance() принимает два аргумента: объект и класс. В приведенном выше примере isinstance() проверяет, является ли miles экземпляром класса Dog, и возвращает True.

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

>>> isinstance(miles, Bulldog)
False

>>> isinstance(jack, Dachshund)
False

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

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

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

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

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

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

Теперь .speak() определена для класса JackRussellTerrier с аргументом по умолчанию для звука, установленным на «Arf».

Обновите dog.py с новым классом JackRussellTerrier и нажмите F5, чтобы сохранить и запустить файл. Теперь вы можете вызвать .speak() для экземпляра JackRussellTerrier без передачи аргумента для звука:

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

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

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

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

Например, в окне редактора измените строку, возвращаемую методом .speak() в классе Dog:

class Dog:
    # Leave other attributes and methods as they are

    # Change the string returned by .speak()
    def speak(self, sound):
        return f"{self.name} barks: {sound}"

Сохраните файл и нажмите F5. Теперь, когда вы создаете новый экземпляр Bulldog с именем jim, jim.speak() возвращает новую строку:

>>> jim = Bulldog("Jim", 5)
>>> jim.speak("Woof")
'Jim barks: Woof'

Однако вызов .speak() на экземпляре JackRussellTerrier не покажет новый стиль вывода:

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

Иногда имеет смысл полностью переопределить метод из родительского класса. Но в данном случае мы не хотим, чтобы класс JackRussellTerrier потерял все изменения, которые могут быть внесены в форматирование выходной строки метода Dog.speak().

Для этого необходимо определить метод .speak() в дочернем классе JackRussellTerrier. Но вместо явного определения выходной строки вам нужно вызвать .speak() класса Dog внутри .speak() дочернего класса, используя те же аргументы, которые вы передали в JackRussellTerrier.speak().

Вы можете получить доступ к родительскому классу внутри метода дочернего класса с помощью функции super():

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return super().speak(sound)

Когда вы вызываете super().speak(sound) внутри JackRussellTerrier, Python ищет в родительском классе Dog метод .speak() и вызывает его с переменной sound.

Обновите dog.py с новым классом JackRussellTerrier. Сохраните файл и нажмите F5, чтобы вы могли проверить его в интерактивном окне:

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

Теперь при вызове miles.speak() вы увидите вывод, отражающий новое форматирование в классе Dog.

Примечание: В приведенных выше примерах иерархия классов очень проста. Класс JackRussellTerrier имеет единственный родительский класс Dog. В реальных примерах иерархия классов может быть довольно сложной.

super() делает гораздо больше, чем просто поиск метода или атрибута в родительском классе. Она обходит всю иерархию классов в поисках подходящего метода или атрибута. Если вы не будете осторожны, super() может привести к неожиданным результатам.

Заключение

В этом мануале из двух частей (первая часть, то есть начало — вот здесь) вы узнали об объектно-ориентированном программировании (ООП) в Python. Большинство современных языков программирования, таких как Java, C# и C++, следуют принципам ООП, поэтому знания, полученные здесь, будут применимы независимо от того, куда приведет вас карьера программиста.

Итак, в этом учебнике вы узнали, как:

  • определять класс, который является своего рода чертежом объекта;
  • инстанцировать объект из класса (создавать инстанс или эксземпляр объекта);
  • использовать атрибуты и методы для определения свойств и поведения объекта;
  • использовать наследование для создания дочерних классов на основе родительского класса;
  • ссылаться на метод родительского класса с помощью super();
  • проверьте, наследует ли объект от другого класса, используя isinstance().

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023