Menu

Наследование в Python: подклассы, super(), переопределение и MRO

Как работает наследование классов в Python: создание подкласса, вызов super().__init__, переопределение и расширение методов, isinstance и issubclass, множественное наследование и MRO, и когда композиция подходит лучше.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Класс, построенный на другом классе

Наследование позволяет определить новый класс как расширение существующего. Существующий класс называется родительским (или базовым, или суперклассом), а новый называется дочерним (подклассом). Дочерний класс получает все методы и атрибуты класса родителя, не повторяя их, и может добавить свои или заменить те, что должны вести себя иначе.

Родитель указывается в скобках после имени класса:

Вывод:

Rex is an animal
Woof

У Dog нет собственных __init__ и describe. Когда ты вызываешь Dog("Rex"), Python ищет __init__ в Dog, не находит и использует тот, что в Animal. Такой же поиск происходит для rex.describe(). Для rex.speak() он находит speak сразу в Dog и на этом останавливается.

В этом порядке поиска и заключается весь механизм: Python ищет в собственном классе объекта, затем в родителе, затем в родителе родителя и берёт первое совпадение. Если классы для тебя в новинку, сначала прочитай страницу про классы: там разобраны __init__, self и методы.

Переопределение метода

Метод дочернего класса с тем же именем, что и метод родителя, заменяет его для экземпляров дочернего класса. Это называется переопределением. Каждый подкласс может ответить на один и тот же вызов по-своему:

Вывод:

Rectangle with area 12
Square with area 25
Shape with area 0

summary определён один раз, в Shape, но вызывает self.area(). Поскольку self это настоящий объект, Rectangle выполняет Rectangle.area, а Square находит area у своего родителя Rectangle. Код родителя вызывает метод потомка, даже не зная о существовании потомка. Именно поэтому наследование удобно, когда общий порядок работы один, а детали заполняет каждый подкласс.

Вызов родителя через super()

Когда дочерний класс определяет собственный __init__, родительский __init__ сам по себе больше не выполняется. Если дочернему классу всё же нужна настройка родителя, вызови её через super().__init__():

Вывод:

Ada 1050.0

super() возвращает объект, который ищет методы начиная с родителя текущего класса. Поэтому super().__init__(owner, balance) выполняет Account.__init__ на том же объекте, и после этого self.owner и self.balance существуют.

Пропусти эту строку, и объект будет собран только наполовину:

Вывод:

0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'

Вместо этого можно написать Account.__init__(self, owner), и в старом коде так часто и делают. super() предпочтительнее, потому что не повторяет имя родителя и правильно работает при множественном наследовании, о котором ниже.

Расширить метод, а не заменить

super() работает в любом методе, а не только в __init__. Если вызвать версию родителя внутри переопределения, можно добавить поведение вокруг неё, а не переписывать её:

Вывод:

started
[12:00] started
INFO [12:00] started

Каждый уровень добавляет одну деталь и передаёт остальное наверх. Если позже format родителя изменится, оба потомка получат это изменение.

isinstance и issubclass

Экземпляр подкласса одновременно является экземпляром каждого класса выше него. isinstance(obj, cls) проверяет это отношение, а issubclass(child, parent) проверяет его между двумя классами:

Предпочитай isinstance прямому сравнению type(obj). Проверка вроде type(obj) is Animal отвергает любой подкласс и ломается, как только кто-то добавит Dog.

Каждый класс, который ты пишешь, наследует от object, даже если ты этого не указываешь. class Animal: то же самое, что class Animal(object):. Отсюда берутся методы по умолчанию вроде __str__, __repr__ и __eq__, и поэтому переопределение __str__ меняет то, что показывает print():

Вывод:

(2, 3)
True

Обязать подклассы реализовать метод

Иногда родитель не должен использоваться напрямую, только как шаблон. Модуль abc помечает такой класс как абстрактный. Любой метод с декоратором @abstractmethod должен быть переопределён, прежде чем из класса можно будет создать объект:

Первые три строки вывода это результат экспорта:

exporting 2 rows
1,2
3,4

Последняя строка это TypeError, в котором названы Exporter и его недостающий метод export (точная формулировка зависит от версии Python). Ошибка проявляется при создании объекта, а не позже, когда export наконец вызовут.

Множественное наследование и MRO

Класс может перечислить больше одного родителя. Тогда он наследует от всех:

Вывод:

paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']

Оба родителя определяют move, и Duck получает версию из Swimmer. Python решает это с помощью порядка разрешения методов (method resolution order, MRO): единого списка классов в Duck.__mro__, который каждый поиск атрибута проходит от начала к концу. Список начинается с самого класса, сохраняет родителей в том порядке, в котором ты их перечислил, ставит каждый класс раньше его собственных родителей и заканчивается object. Поменяй порядок на class Duck(Flyer, Swimmer), и move вернёт "flies".

super() следует тому же списку. Внутри метода Swimmer вызов super() означает «следующий класс после Swimmer в MRO класса объекта», и для Duck это Flyer, а не object. Благодаря этому кооперативные классы могут каждый вызывать super(), и каждый класс в цепочке выполнится ровно один раз, даже в ромбе, где у двух родителей общий предок.

На практике множественное наследование используют в основном для миксинов: небольших классов, которые добавляют одну возможность (например, JsonMixin добавляет метод to_json) и сочетаются с одним основным родителем. Глубокие иерархии с несколькими полноценными родителями трудно понять, и нужны они редко.

Наследование или композиция?

Наследование описывает отношение «является» (is a). SavingsAccount является Account: любой код, работающий с Account, должен работать и с ним без изменений. Если эта фраза звучит странно, тебе, скорее всего, нужна композиция, которая описывает отношение «содержит» (has a): объект хранит другой объект в атрибуте и вызывает его.

Вывод:

Roadster: 300 hp engine running

class Car(Engine) тоже бы работал, но утверждал бы, что автомобиль это разновидность двигателя, дал бы Car все методы двигателя и не позволил бы поставить другой двигатель без другого класса. При композиции части остаются раздельными, и чтобы заменить двигатель, достаточно передать другой объект.

Несколько признаков того, что наследование здесь не тот инструмент:

  • Дочерний класс переопределяет большинство методов родителя или отключает некоторые из них.
  • Ты наследуешь только ради одного или двух вспомогательных методов.
  • Иерархия разрастается дальше двух или трёх уровней, и чтобы понять, что делает метод, приходится читать несколько файлов.

Частые ошибки

  • Забыть super().__init__(). __init__ дочернего класса заменяет родительский, поэтому атрибуты родителя так и не задаются, и позже ты получаешь AttributeError.
  • Передавать self в вызовы super(). Пиши super().__init__(name), а не super().__init__(self, name). super() уже привязывает метод к текущему объекту.
  • Несовместимые сигнатуры при переопределении. Если общий код вызывает родительский speak(self), то speak(self, volume) в дочернем классе с обязательным дополнительным аргументом этот код сломает. Делай переопределения совместимыми или давай новым параметрам значения по умолчанию.
  • Проверять type(obj) == SomeClass. Такая проверка игнорирует подклассы. Используй isinstance.
  • Наследовать, чтобы поделиться кодом между несвязанными классами. Лучше вынеси общий код в функцию или вспомогательный объект.

Часто задаваемые вопросы

Как унаследовать класс в Python?

Укажи родительский класс в скобках после имени нового класса: class Dog(Animal):. Dog получает все методы и атрибуты класса Animal и может добавлять новые или переопределять существующие.

Что делает super().__init__()?

Вызывает __init__ родительского класса, чтобы родитель настроил свои атрибуты. Когда подкласс определяет собственный __init__, Python не вызывает родительский автоматически, поэтому ты вызываешь super().__init__(...) с аргументами, которые нужны родителю, а затем задаёшь дополнительные атрибуты подкласса.

Чем isinstance отличается от type в Python?

isinstance(obj, Animal) равен True для Animal и для экземпляров любого подкласса Animal. type(obj) is Animal равен True, только когда объект создан от самого Animal. Предпочитай isinstance: он продолжает работать, когда кто-то добавит подкласс.

Поддерживает ли Python множественное наследование?

Да: class C(A, B): наследует от обоих. Когда оба родителя определяют один и тот же метод, Python выбирает один, следуя порядку разрешения методов (MRO), который можно посмотреть через C.__mro__. В нём сначала идёт C, затем его родители слева направо, а последним object.

Когда использовать композицию вместо наследования?

Наследование подходит для отношения «является» (is a), когда подкласс можно использовать везде, где ожидается родитель (SavingsAccount является Account). Композиция, то есть хранение объекта в атрибуте, подходит для отношения «содержит» (has a): у Car есть Engine. Если сомневаешься, композицию проще изменить потом.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ