Когда вы создаете приложение Python, есть одна вещь, которая может значительно облегчить вам жизнь: наследование классов. Давайте научимся его использовать.
Наследование классов позволяет создавать классы на основе других классов с целью повторного использования уже реализованного кода Python вместо необходимости повторной реализации аналогичного кода.
Первые два понятия, которые следует изучить в наследовании в Python, — это класс Parent и класс Child.
Что такое родительский класс?
Родительский класс — это класс, от которого вы наследуете в своей программе, чтобы повторно использовать его код. Его также называют базовым классом или суперклассом.
Что такое дочерний класс?
Дочерний класс — это класс, который наследует родительский класс. Его также называют производным классом или подклассом.
Мы поработаем над простой футбольной игрой и покажем, как работает наследование в Python.
Но сначала давайте начнем с некоторых очень важных основ наследования!
Разница между родительским и дочерним классом
Мы говорили о занятиях для родителей и детей…
В чем разница между этими двумя понятиями с точки зрения того, как мы их определяем?
Давайте начнем с определения класса с именем A так, как мы определяем любой класс в Python. Чтобы упростить этот пример, мы просто будем использовать оператор pass в реализации каждого класса.
Что такое оператор pass в классе Python?
Оператор pass используется в классах Python для определения класса без реализации в нем какого-либо кода (например, атрибутов и методов). Использование оператора pass является распространенным приемом для создания структуры вашей программы и избежания ошибок, возникающих в интерпретаторе из-за отсутствия реализации в классе.
Я использую оператор pass, потому что не хочу, чтобы вы сейчас сосредотачивались на коде классов, а сосредоточились только на самой концепции наследования.
class A:
pass
Класс А — это просто обычный класс.
Какую ошибку мы увидим, если не включим оператор pass в наш класс?
$ python inheritance.py
File "inheritance.py", line 2
^
SyntaxError: unexpected EOF while parsing
Интерпретатор Python не любит код, содержащий только первую строку определения класса A без pass.
Итак, возвращаясь к нашему рабочему примеру. Что делает класс A родительским классом?
Тот факт, что в нашей программе мы создаем класс с именем B, который наследует (или выводит) из него:
class B(A):
pass
Обратите внимание, что после имени класса BI также включил в скобки класс A. Это означает, что B наследуется от A. Другими словами, B является дочерним классом, а A — его родительским классом.
Но это еще не все…
Мы можем определить класс с именем C, который наследует от B:
class C(B):
pass
Вы видите, что роль класса в вопросах наследования не является раз и навсегда определенной… что я имею в виду?
Класс может быть как родительским, так и дочерним классом, как мы видели на примере класса B в нашем примере.
Это очень мощный инструмент, позволяющий создавать сложные программы на Python с использованием всего лишь нескольких строк кода.
А теперь давайте рассмотрим практический пример наследования.
Первый пример наследования классов в Python
Во-первых, мы создаем базовый класс с именем Player. Его конструктор принимает имя и вид спорта:
class Player:
def __init__(self, name, sport):
self.name = name
self.sport = sport
Мы могли бы использовать класс Player как родительский класс, из которого мы можем вывести классы для игроков в разных видах спорта. Давайте создадим дочерние классы, которые представляют футболистов.
Я определю четыре детских класса для четырех футбольных амплуа: вратарь, защитник, полузащитник и нападающий.
Как мы уже видели, вот как создать класс в Python, который наследует от другого класса. Мы используем оператор class и дополнительно указываем имя класса, от которого хотим наследовать, после имени нашего класса в скобках:
class ChildClass(ParentClass):
...
...
Даже если мы не определяем никаких методов в нашем дочернем классе, мы все равно можем вызывать методы из родительского класса, как если бы они были реализованы в дочернем. В этом и заключается сила наследования.
Мы определим наши дочерние классы таким образом, что атрибут спорта будет установлен автоматически. Вот пример для класса Goalkeeper:
class Goalkeeper(Player):
def __init__(self, name):
super().__init__(name, 'football')
Как видите, родительский класс Player находится в скобках.
Затем мы определяем метод __init__
(конструктор), который заменяет метод __init__
, унаследованный от родительского класса.
Если метод __init__
не определен в дочернем классе, то автоматически используется метод __init__
из родительского класса.
В конструкторе мы используем метод super(), который ссылается на родительский класс. Мы используем его для вызова конструктора родительского класса и передаем ему:
- Имя игрока, указанное при создании объекта типа «Вратарь».
- Вид спорта «футбол».
То же самое относится ко всем ролям:
class Player:
def __init__(self, name, sport):
self.name = name
self.sport = sport
class Goalkeeper(Player):
def __init__(self, name):
super().__init__(name, 'football')
class Defender(Player):
def __init__(self, name):
super().__init__(name, 'football')
class Midfielder(Player):
def __init__(self, name):
super().__init__(name, 'football')
class Striker(Player):
def __init__(self, name):
super().__init__(name, 'football')
Теперь давайте создадим объект типа Striker:
striker1 = Striker('James Striker')
print(striker1.__dict__)
Как видите, пространство имен нового объекта содержит атрибуты name и role:
{'name': 'James Striker', 'sport': 'football'}
Следующим шагом будет добавление нового атрибута к нашим классам.
Добавление атрибута к дочернему классу
Пришло время добавить атрибут к нашим дочерним классам. Атрибут, который применяется только к футболистам, а не обязательно ко всем игрокам в спорте.
Это сила наследования. Мы можем наследовать функциональность от родительского класса, а затем предоставлять дополнительные функции, которые специфичны только для дочерних классов. Это позволяет избежать повторения кода, который уже есть в родительском классе.
Одним из атрибутов, присущим только футболистам, но не применимым ко всем игрокам, является амплуа.
Например, добавим роль в наш класс Striker:
class Striker(Player):
def __init__(self, name):
super().__init__(name, 'football')
self.role = 'striker'
Теперь мы можем увидеть атрибут роли в пространстве имен экземпляра дочернего класса:
>>> striker1 = Striker('James Striker')
>>> print(striker1.__dict__)
{'name': 'James Striker', 'sport': 'football', 'role': 'striker'}
Этот код работает, но он не универсален…
Что, если мы хотим создать объект типа Вратарь, Защитник или Полузащитник?
Чтобы сделать его универсальным, нам нужно добавить новый атрибут в конструктор каждого дочернего класса.
Так, например, класс Striker становится:
class Striker(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
Нам нужно не забыть включить роль при создании объекта нападающего, иначе мы получим следующую ошибку:
$ python football.py
Traceback (most recent call last):
File "football.py", line 28, in <module>
striker1 = Striker('James Striker')
TypeError: __init__() missing 1 required positional argument: 'role'
Итак, вот как мы теперь создаем объект-нападающего:
striker1 = Striker('James Striker', 'striker')
Круто! Наши занятия потихоньку становятся лучше.
Добавление метода в родительский класс
А теперь…
…давайте добавим метод с именем play в наш родительский класс:
class Player:
def __init__(self, name, sport):
self.name = name
self.sport = sport
def play(self):
pass
Определенный мной метод включает только оператор pass, который, как мы видели ранее, в Python ничего не делает.
Так почему же мы добавляем это в метод?
Давайте создадим объект типа Player и запустим метод play:
player1 = Player('Player1', 'football')
player1.play()
Вы увидите, что при запуске этого кода вы не получите никаких выходных данных от метода play.
Давайте попробуем удалить оператор pass из метода и посмотрим, что произойдет, если мы выполним тот же код, что и выше:
$ python football.py
File "football.py", line 9
class Goalkeeper(Player):
^
IndentationError: expected an indented block
На этот раз Python выдает ошибку отступа, вызванную отсутствием кода внутри метода play (который непосредственно предшествует определению класса Goalkeeper.
Итак, мы добавим сообщение print в метод play родительского класса и перейдем к реализации того же метода для некоторых дочерних классов.
Вот как выглядят все наши занятия на данный момент:
class Player:
def __init__(self, name, sport):
self.name = name
self.sport = sport
def play(self):
print("Player {} starts running".format(self.name))
class Goalkeeper(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
class Defender(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
class Midfielder(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
class Striker(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
Теперь мы можем увидеть, как метод play наследуется дочерним классом. Давайте создадим объект типа Midfielder и выполним на нем метод play:
midfielder1 = Midfielder('James Midfielder', 'midfielder')
midfielder1.play()
Вывод:
$ python football.py
Player James Midfielder starts running
Когда мы вызываем метод play на объекте Midfielder, вызывается метод play класса Player. Это связано с порядком разрешения методов.
Порядок разрешения методов (MRO) — это порядок, в котором Python ищет метод в иерархии классов.
Вы можете использовать метод mro() класса, чтобы увидеть порядок разрешения:
print(Midfielder.mro())
[<class '__main__.Midfielder'>, <class '__main__.Player'>, <class 'object'>]
Вывод показывает, что в этом случае Python использует следующий порядок разрешения методов:
- Класс полузащитника.
- Класс игрока.
- класс объекта, от которого наследуется большинство классов в Python.
Итак, в нашем сценарии Python не находит метод play в классе Midfielder и использует тот же метод из родительского класса Player.
Переопределить метод в классе Python
Переопределение метода означает определение метода в дочернем классе с тем же именем, что и у одного из методов в родительском классе.
В этом случае мы можем определить метод игры в классе Полузащитник следующим образом:
class Midfielder(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
def play(self):
print("Player {} passes the ball to a striker".format(self.name))
На этот раз сообщение о печати более конкретное: в нем говорится, что полузащитник передает мяч нападающему, а не выводится общее сообщение, которое применимо ко всем типам игроков.
Давайте выполним этот метод на объекте типа Полузащитник таким же образом, как мы это делали в предыдущем разделе:
midfielder1 = Midfielder('James Midfielder', 'midfielder')
midfielder1.play()
Вывод:
$ python football.py
Player James Midfielder passes the ball to a striker
На этот раз Python выполняет метод дочернего класса Midfielder, поскольку он реализован в нем, и не выполняет тот же метод родительского класса (следуя порядку разрешения методов).
Вызов родительского метода из дочернего класса
Мы видели, как дочерний класс Midfielder автоматически разрешал метод воспроизведения из родительского класса, когда у него не было реализации для него.
Но существуют ли сценарии, в которых нам может потребоваться явно вызвать родительский метод из дочернего класса, даже если тот же метод существует в дочернем классе?
Давайте узнаем!
Я хочу изменить код так, чтобы при выполнении метода play в одном из дочерних классов выводились два сообщения:
- В первом сообщении говорится, что игрок начинает бежать.
- Второе сообщение описывает следующее действие, которое предпримет наш игрок.
При этом мы хотим использовать тот факт, что первое сообщение уже выведено методом play родительского класса, и мы хотим избежать его повторения в дочерних классах:
Например, давайте обновим метод play класса Midfielder:
class Midfielder(Player):
def __init__(self, name, role):
super().__init__(name, 'football')
self.role = role
def play(self):
super().play()
print("Player {} passes the ball to a striker".format(self.name))
Во-первых, в методе play мы используем super() для вызова метода play родительского класса. А затем мы выполняем оператор print, чтобы показать второе действие, предпринятое нашим полузащитником.
И вот что мы видим, когда запускаем метод play на объекте типа Midfielder:
$ python football.py
Player James Midfielder starts running
Player James Midfielder passes the ball to a striker
В этом примере я использую Python 3.
$ python --version
Python 3.7.4
Мне интересно, работает ли это также с Python 2…
$ python2 --version
Python 2.7.14
$ python2 football.py
Traceback (most recent call last):
File "football.py", line 39, in <module>
midfielder1 = Midfielder('James Midfielder', 'midfielder')
File "football.py", line 25, in __init__
super().__init__(name, 'football')
TypeError: super() takes at least 1 argument (0 given)
Мы можем увидеть ошибку при вызове super() без аргументов, если используем Python 2.
Это потому что…
В Python 2 метод super() требует дополнительных аргументов по сравнению с Python 3. Нам также необходимо явно наследовать родительский класс от object, как показано ниже.
class Player(object):
def __init__(self, name, sport):
self.name = name
self.sport = sport
def play(self):
print("Player {} starts running".format(self.name))
...
...
...
class Midfielder(Player):
def __init__(self, name, role):
super(Midfielder, self).__init__(name, 'football')
self.role = role
def play(self):
super(Midfielder, self).play()
print("Player {} passes the ball to a striker".format(self.name))
Я объясню точное обоснование этого в другой статье о разнице между классами старого и нового стилей в Python.
На данный момент обратите внимание на следующие изменения…
Определение родительского класса теперь начинается со слов:
class Player(object):
А два вызова super принимают два аргумента: подкласс, в котором вызывается super(), и экземпляр подкласса:
super(Midfielder, self).__init__(name, 'football')
super(Midfielder, self).play()
В следующих разделах этого руководства мы продолжим использовать синтаксис Python 3 для вызова метода super.
Разница между isinstance и issubclass с классами Python
Давайте углубим наши знания о классах Python в связи с наследованием.
В этом последнем разделе мы рассмотрим разницу между встроенными функциями Python isinstance и issubclass.
Разница между этими двумя функциями объясняется в их названии:
- isinstance применяется к экземплярам. Позволяет проверить тип экземпляра класса (или объекта).
- i ssubclass применяется к классам. Он предоставляет подробную информацию о наследовании между классами.
Начнем с isinstance…
Функция isinstance принимает два аргумента в следующем порядке: object и classinfo. Она возвращает True, если объект является экземпляром classinfo или его подклассом. В противном случае она возвращает False.
Вот что он возвращает, когда мы применяем его к нашему объекту midfielder1, определенному в предыдущем разделе:
>>> print(isinstance(midfielder1, Midfielder))
True
>>> print(isinstance(midfielder1, Player))
True
Как видите, функция возвращает True в обоих случаях, поскольку midfielder1 является экземпляром типа Midfielder, но также и типа Player из-за наследования.
А теперь давайте посмотрим на issubclass…
Функция issubclass принимает два аргумента: class и classinfo. Она возвращает True, если класс является подклассом classinfo. В противном случае она возвращает False.
Применим его к классам «Полузащитник» и «Игрок»:
>>> print(issubclass(Midfielder, Midfielder))
True
>>> print(issubclass(Midfielder, Player))
True
Мы уже знали, что Midfielder является подклассом Player. Но с кодом выше мы также узнали, что Midfielder является подклассом Midfielder.
Класс является подклассом самого себя.
Все ясно?
Заключение
В этой статье мы рассмотрели довольно многое…
Вы узнали:
- Основы наследования в Python.
- Разница между родительскими и дочерними классами.
- Способ определения методов в дочерних классах, которые переопределяют те же методы из родительских классов.
- Метод вызова родительских методов из дочерних классов.
- Разница между встроенными методами Python isinstance и issubclass.
А вы? Как вы используете наследование в своих программах на Python?