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

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

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

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

Языки, реализующие инструментарий для объектно ориентированного программирования, использующий классы, как правило предоставляют и механизм наследования. 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 = NonDecreasingConter()
>>> n.inc()
>>> n.inc()
>>> n.value
2
>>> n.dec()
>>> n.value
2

В объявлении NonDecreasingConter присутствует метод dec, а вот откуда взялись value и inc? Они были взяты от предка — класса Counter! Сий факт даже можно пронаблюдать:

>>> n.dec
<bound method NonDecreasingConter.dec of <__main__.NonDecreasingConter object at 0x7f361b29c940>>
>>> n.inc
<bound method Counter.inc of <__main__.NonDecreasingConter object at 0x7f361b29c940>>

Метод dec — метод класса NonDecreasingConter, связанный с конкретным экземпляром NonDecreasingConter. А вот 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 так названа в честь названия класса-предка: "superclass".

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

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

class Foo:
    pass

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

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

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

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →