Python: Введение в ООП
Теория: Наследование
Все классы, которые мы рассматривали до этого, создавались "с нуля". И до тех пор, пока описываемые классами сущности мало похожи друг на друга, создание абсолютно новых классов работает отлично. Но что делать, если мы хотим, чтобы пара классов содержала один и тот же метод — не одноименный, а именно копию?
Конечно же, мы можем при объявлении класса вместо объявления метода по месту поместить в атрибут ссылку на существующую функцию. И это даже сработает. Но когда таковых методов станет несколько, уследить за тем, что и куда копируется, станет очень сложно. К счастью, есть способ лучше.
Языки, реализующие инструментарий для объектно ориентированного программирования, включая использование классов, предоставляют и механизм наследования. Python — один из таких языков. Поэтому классы в Python можно наследовать.
Когда один класс становится наследником другого, то все атрибуты класса-предка (надкласса, superclass) становятся доступны классу-потомку (подклассу, subclass) — наследуются (достаются в наследство).
Что дает наследование
Наследование позволяет выделить общее для нескольких классов поведение и вынести его в отдельную сущность. То есть наследование является средством переиспользования кода (code reuse) — использования существующего кода для решения новых задач.
Наследование позволяет получить новый класс, немного отличающийся от старого. При этом нам не нужно иметь доступ к коду исходного класса, а значит с помощью наследования мы можем адаптировать (использовать повторно) под наши задачи, в том числе и чужие классы.
Как обычно, рассмотрим пример:
Если мы выполним эти объявления классов и посмотрим на поведение экземпляра NonDecreasingCounter, то увидим, что он работает как Counter — имеет те же методы и атрибуты (правда, при вызове метода .dec новый счетчик не изменяет текущее значение):
В объявлении NonDecreasingCounter присутствует метод dec, а вот откуда взялись value и inc? Они были взяты от предка — класса Counter. Данный факт даже можно пронаблюдать:
Метод dec — метод класса NonDecreasingCounter, связанный с конкретным экземпляром NonDecreasingCounter. А вот inc — метод класса Counter, хоть и связанный с все тем же экземпляром класса-потомка.
Здесь вы можете увидеть сходство с взаимоотношениями между классом и его экземпляром: если экземпляр получает свой собственный атрибут, то этот атрибут заменяет атрибут класса. Точно так же объявления в классе-потомке заменяют собой атрибуты класса-предка, если имя используется то же самое — говорят, переопределяют (override).
И, как и в случае с объектом, который может использовать все содержимое класса и заменять только небольшую часть атрибутов (или добавлять новые.), так и потомок по умолчанию получает все атрибуты предка, часть из которых может изменить.
Все будет super()
Представим, что нас в целом устраивает класс Counter из предыдущего примера, но мы хотим при вызове inc увеличивать значение дважды. Мы могли бы заменить в потомке весь метод и прописать внутри нового метода self.value += 2. Но если бы позже что-то поменялось в исходном классе Counter, то эти изменения не коснулись бы нашего метода.
Получается, что нам внутри метода потомка нужно получить доступ к методу предка. Методу с тем же именем. Если мы просто обратимся к self.inc, то получим ссылку на новый метод, ведь мы его переопределили.
Тут нам на помощь приходит специальная функция super. Функция super так названа в честь названия класса-предка: "superclass":
Вызов super здесь заменяет обращение к self. При этом вы фактически обращаетесь к "памяти предков": получаете ссылку на атрибут предка. Более того, в данном случае, super().inc - это связанный с текущим экземпляром метод, то есть полноценная "оригинальная версия" из класса-предка. Если бы вы вдруг решили вручную вызвать метод класса предка, то вам бы пришлось использовать его не связанную версию:
Вызов super вместо явного вызова предка хорош не только тем, что автоматически связывает методы. При смене предка (такое бывает) в описании класса super учтет изменения, и вы получите доступ к поведению нового предка. Удобно.
super работает не только с методами, но и с атрибутами классов:
Но важно помнить, что super работает именно с классами. Вы не сможете получить доступ к атрибутам, которые добавляются в объект уже после того, как тот будет создан.
Вызов инициализатора суперкласса с super()
При наследовании классов часто возникает необходимость не только добавить новые атрибуты или методы, но и расширить или изменить инициализацию объекта. В этом случае очень важно корректно вызвать конструктор суперкласса, чтобы все атрибуты и состояние, которые должны быть наследованы, были правильно установлены.
Использование super() в __init__ позволяет нам вызвать конструктор суперкласса, что гарантирует, что весь необходимый код инициализации будет выполнен:
В этом примере метод __init__ в NonDecreasingCounter вызывает метод __init__ предка Counter с помощью super(). Это гарантирует, что атрибут value инициализируется как в Counter. Класс NonDecreasingCounter добавляет дополнительный атрибут non_decreasing и изменяет поведение метода dec, чтобы контролировать, может ли счетчик уменьшаться. Это демонстрирует, как можно расширить и настроить поведение классов при наследовании.
В контексте множественного наследования использование super() становится еще более важным, так как оно гарантирует, что все конструкторы суперклассов вызываются в правильном порядке. Это предотвращает проблемы с инициализацией и позволяет каждому классу в иерархии наследования вносить свой вклад в конечное состояние объекта.
Наследование и object
В прошлом мы не указывали предка в объявлениях классов, то есть писали так:
В Python3 такая запись равнозначна записи class Foo(object):. То есть, если класс-предок не указан, то таковым считается object — самый базовый класс в Python. Сейчас, в эпоху повсеместного использования Python3, указывать или не указывать наследование от object — дело вкуса.
А вот в Python2 class Foo: и class Foo(object): не были равнозначны. И это приводило к очень неприятным последствиям. Поэтому до сих пор можно встретить линтеры, которые жалуются на код без (object) — вдруг вы захотите запустить код на старом добром втором Python?
Вам решать, будете ли вы указывать предка object или отключите соответствующее предупреждение. Благо в Python3 оба варианта приемлемы и не противоречат друг другу.

.png)


