- Что дает наследование
- Все будет super()
- Вызов инициализатора суперкласса с super()
- Наследование и object
Все классы, которые мы рассматривали до этого, создавались "с нуля". И до тех пор, пока описываемые классами сущности мало похожи друг на друга, создание абсолютно новых классов работает отлично. Но что делать, если мы хотим, чтобы пара классов содержала один и тот же метод — не одноименный, а именно копию?
Конечно же, мы можем при объявлении класса вместо объявления метода по месту поместить в атрибут ссылку на существующую функцию. И это даже сработает! Но когда таковых методов станет несколько, уследить за тем, что и куда копируется, станет очень сложно. К счастью, есть способ лучше!
Языки, реализующие инструментарий для объектно ориентированного программирования, включая использование классов, предоставляют и механизм наследования. Python — один из таких языков. Поэтому классы в Python можно наследовать.
Когда один класс становится наследником другого, то все атрибуты класса-предка (надкласса, superclass) становятся доступны классу-потомку (подклассу, subclass) — наследуются (достаются в наследство).
Что дает наследование
Наследование позволяет выделить общее для нескольких классов поведение и вынести его в отдельную сущность. То есть наследование является средством переиспользования кода (code reuse) — использования существующего кода для решения новых задач!
Наследование позволяет получить новый класс, немного отличающийся от старого. При этом нам не нужно иметь доступ к коду исходного класса, а значит с помощью наследования мы можем адаптировать (использовать повторно) под наши задачи, в том числе и чужие классы!
Как обычно, рассмотрим пример:
# этот класс у нас уже был
class Counter:
def __init__(self):
self.value = 0
def inc(self):
self.value += 1
def dec(self):
self.value -= 1
# А этот класс - новый. Наследник Counter
class NonDecreasingCounter(Counter): # в скобках указан класс-предок
def dec(self):
pass
Если мы выполним эти объявления классов и посмотрим на поведение экземпляра NonDecreasingCounter
, то увидим, что он работает как Counter
— имеет те же методы и атрибуты (правда, при вызове метода .dec
новый счетчик не изменяет текущее значение):
n = NonDecreasingCounter()
n.inc()
n.inc()
n.value # 2
n.dec()
n.value # 2
В объявлении NonDecreasingCounter
присутствует метод dec
, а вот откуда взялись value
и inc
? Они были взяты от предка — класса Counter
! Данный факт даже можно пронаблюдать:
n.dec
# <bound method NonDecreasingCounter.dec of <__main__.NonDecreasingCounter object at 0x7f361b29c940>>
n.inc
# <bound method Counter.inc of <__main__.NonDecreasingCounter object at 0x7f361b29c940>>
Метод dec
— метод класса NonDecreasingCounter
, связанный с конкретным экземпляром NonDecreasingCounter
. А вот inc
— метод класса Counter
, хоть и связанный с все тем же экземпляром класса-потомка.
Здесь вы можете увидеть сходство с взаимоотношениями между классом и его экземпляром: если экземпляр получает свой собственный атрибут, то этот атрибут заменяет атрибут класса. Точно так же объявления в классе-потомке заменяют собой атрибуты класса-предка, если имя используется то же самое — говорят, переопределяют (override).
И, как и в случае с объектом, который может использовать все содержимое класса и заменять только небольшую часть атрибутов (или добавлять новые!), так и потомок по умолчанию получает все атрибуты предка, часть из которых может изменить.
Все будет super()
Представим, что нас в целом устраивает класс Counter
из предыдущего примера, но мы хотим при вызове inc
увеличивать значение дважды. Мы могли бы заменить в потомке весь метод и прописать внутри нового метода self.value += 2
. Но если бы позже что-то поменялось в исходном классе Counter
, то эти изменения не коснулись бы нашего метода.
Получается, что нам внутри метода потомка нужно получить доступ к методу предка. Методу с тем же именем! Если мы просто обратимся к self.inc
, то получим ссылку на новый метод, ведь мы его переопределили.
Тут нам на помощь приходит специальная функция super
. Функция super
так названа в честь названия класса-предка: "superclass":
class DoubleCounter(Counter):
def inc(self):
super().inc()
super().inc()
Вызов super
здесь заменяет обращение к self
. При этом вы фактически обращаетесь к "памяти предков": получаете ссылку на атрибут предка. Более того, в данном случае, super().inc
- это связанный с текущим экземпляром метод, то есть полноценная "оригинальная версия" из класса-предка. Если бы вы вдруг решили вручную вызвать метод класса предка, то вам бы пришлось использовать его не связанную версию:
class DoubleCounter(Counter):
def inc(self):
Counter.inc(self) # явно обращаемся к методу класса предка
Counter.inc(self) # и передаем ссылку на экземпляр
Вызов super
вместо явного вызова предка хорош не только тем, что автоматически связывает методы. При смене предка (такое бывает) в описании класса super
учтет изменения, и вы получите доступ к поведению нового предка. Удобно!
super
работает не только с методами, но и с атрибутами классов:
class A:
x = 'A'
class B(A):
x = 'B'
def super_x(self):
return super().x
B().x # 'B'
B().super_x() # 'A'
Но важно помнить, что super
работает именно с классами. Вы не сможете получить доступ к атрибутам, которые добавляются в объект уже после того, как тот будет создан.
class A:
x = 'A'
class B(A):
def super_x(self):
return super().x
def super_y(self):
return super().y
a = A()
b = B()
# добавляем атрибут после создания объекта
a.y = 42
b.super_x() # 'A'
b.super_y() # AttributeError ...
Вызов инициализатора суперкласса с super()
При наследовании классов часто возникает необходимость не только добавить новые атрибуты или методы, но и расширить или изменить инициализацию объекта. В этом случае очень важно корректно вызвать конструктор суперкласса, чтобы все атрибуты и состояние, которые должны быть наследованы, были правильно установлены.
Использование super()
в __init__
позволяет нам вызвать конструктор суперкласса, что гарантирует, что весь необходимый код инициализации будет выполнен:
class Counter:
def __init__(self):
self.value = 0
def inc(self):
self.value += 1
def dec(self):
self.value -= 1
class NonDecreasingCounter(Counter):
def __init__(self):
super().__init__() # Вызываем конструктор предка
self.non_decreasing = True # Дополнительный атрибут для наследника
def dec(self):
if self.non_decreasing:
print("Уменьшение значения запрещено.")
else:
super().dec() # Вызываем метод dec предка, если уменьшение разрешено
n = NonDecreasingCounter()
n.inc()
print(n.value) # 1
n.dec() # Уменьшение значения запрещено.
print(n.value) # 1
n.non_decreasing = False
n.dec()
print(n.value) # 0
В этом примере метод __init__
в NonDecreasingCounter
вызывает метод __init__
предка Counter
с помощью super()
. Это гарантирует, что атрибут value
инициализируется как в Counter
. Класс NonDecreasingCounter
добавляет дополнительный атрибут non_decreasing
и изменяет поведение метода dec
, чтобы контролировать, может ли счетчик уменьшаться. Это демонстрирует, как можно расширить и настроить поведение классов при наследовании.
В контексте множественного наследования использование super()
становится еще более важным, так как оно гарантирует, что все конструкторы суперклассов вызываются в правильном порядке. Это предотвращает проблемы с инициализацией и позволяет каждому классу в иерархии наследования вносить свой вклад в конечное состояние объекта.
Наследование и object
В прошлом мы не указывали предка в объявлениях классов, то есть писали так:
class Foo:
pass
В Python3 такая запись равнозначна записи class Foo(object):
. То есть, если класс-предок не указан, то таковым считается object
— самый базовый класс в Python. Сейчас, в эпоху повсеместного использования Python3, указывать или не указывать наследование от object
— дело вкуса.
А вот в Python2 class Foo:
и class Foo(object):
не были равнозначны! И это приводило к очень неприятным последствиям. Поэтому до сих пор можно встретить линтеры, которые жалуются на код без (object)
— вдруг вы захотите запустить код на старом добром втором Python?
Вам решать, будете ли вы указывать предка object
или отключите соответствующее предупреждение. Благо в Python3 оба варианта приемлемы и не противоречат друг другу.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.