Класс, построенный на другом классе
Наследование позволяет определить новый класс как расширение существующего. Существующий класс называется родительским (или базовым, или суперклассом), а новый называется дочерним (подклассом). Дочерний класс получает все методы и атрибуты класса родителя, не повторяя их, и может добавить свои или заменить те, что должны вести себя иначе.
Родитель указывается в скобках после имени класса:
Вывод:
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. Если сомневаешься, композицию проще изменить потом.