Наследование

Наследование

Все классы, которые мы рассматривали до этого, создавались "с нуля". И до тех пор, пока описываемые классами сущности мало похожи друг на друга, создание абсолютно новых классов работает отлично. Но что делать, если мы хотим, чтобы пара классов содержала один и тот же метод — не одноимённый, а именно копию?

Конечно же, мы можем при объявлении класса вместо объявления метода по месту поместить в атрибут ссылку на существующую функцию. И это даже сработает! Но когда таковых методов станет несколько, уследить за тем, что и куда копируется, станет очень сложно. К счастью, есть способ лучше!

Языки, реализующие инструментарий для объектно ориентированного программирования, использующий классы, как правило предоставляют и механизм наследования. 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:

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 работает именно с классами. Вы не сможете получить доступ к атрибутам, которые добавляются в объект уже после того, как тот будет создан!

Функция super так названа в честь названия класса-предка: "superclass".

Наследование и object

В прошлом мы не указывали предка в объявлениях классов, то есть писали так:

class Foo:
    pass

В Python3 такая запись равнозначна записи class Foo(object):. То есть, если класс-предок не указан, то таковым считается object — самый базовый класс в Python. Сейчас, в эпоху повсеместного использования Python3, указывать или не указывать наследование от object — дело вкуса.

А вот в Python2 class Foo: и class Foo(object): не были равнозначны! И это приводило к очень неприятным последствиям. Поэтому до сих пор можно встретить линтеры, которые жалуются на код без (object) — вдруг вы захотите запустить код на старом добром втором пайтоне?

Линтер, который используем мы в Hexlet, как раз из таких — он будет педантично сообщать о каждом классе, который не унаследован явно от object. Вам решать, будете ли вы указывать предка object или отключите соответствующее предупреждение. Благо в Python3 оба варианта приемлемы и не противоречат друг другу!

Для полного доступа к курсу, нужна профессиональная подписка

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

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».