Как работает наследование в Python?
Класс наследует атрибуты и методы от базового. Поиск атрибута идёт по MRO (cls.__mro__), super() вызывает следующий по MRO. Поддерживается множественное наследование (C3-линеаризация). Часто лучше композиция: дешевле менять и тестировать.
Базовая механика
Класс-наследник получает доступ ко всем атрибутам/методам предков. Когда происходит обращение obj.attr:
- Сначала Python ищет
attrвobj.__dict__(instance attribute) — кроме data descriptors. - Затем по
type(obj).__mro__(Method Resolution Order) обходит классы слева направо: own class → parents → ... →object. - Если найден descriptor (например, function, property, classmethod) — применяется descriptor protocol, метод биндится к экземпляру.
- Если не найдено — вызывается
__getattr__(если определён), иначеAttributeError.
MRO вычисляется один раз при создании класса по алгоритму C3 (см. отдельный вопрос). При множественном наследовании C3 гарантирует local precedence order и monotonicity, иначе бросает TypeError.
super()
super() возвращает прокси-объект, который ищет атрибут начиная со следующего класса по MRO текущего экземпляра. Это позволяет кооперативное наследование: каждый __init__ вызывает super().__init__(), и цепочка инициализаций проходит по всему MRO.
Пример
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
# Можно переопределить полностью или дёрнуть родителя
base = super().speak()
return f"{base} woof"
d = Dog("Rex")
print(d.speak()) # ... woof
print(Dog.__mro__) # (Dog, Animal, object)
print(isinstance(d, Animal)) # True
# Множественное наследование + кооперативный super
class Walker:
def __init__(self, **kw):
print("Walker.__init__")
super().__init__(**kw)
class Swimmer:
def __init__(self, **kw):
print("Swimmer.__init__")
super().__init__(**kw)
class Duck(Walker, Swimmer, Animal):
def __init__(self, name: str):
super().__init__(name=name)
Duck("Donald")
# Walker.__init__
# Swimmer.__init__
# (Animal.__init__ принимает name → self.name = 'Donald')
print(Duck.__mro__)
# Duck → Walker → Swimmer → Animal → object
Что наследуется и что нет
- Наследуются: все методы (включая dunder), class-level атрибуты,
__slots__(с нюансами). __init_subclass__вызывается при создании подкласса — удобно для регистрации/валидации.- Class-level mutable атрибут (
options = []) разделяется между всеми наследниками — частый источник багов. - Static-методы и class-методы наследуются и работают через MRO.
Композиция vs наследование
- Наследование уместно, если есть истинное «is-a» и LSP не нарушается:
DogisAnimal. - Композиция — когда «has-a» или просто шарим поведение:
UserServicehasUserRepository. - Mixin — узкие добавки поведения без состояния:
JSONSerializableMixin,TimestampMixin. - Для интерфейсов лучше Protocol/ABC, чем абстрактные базовые классы с шаблонными методами.
Подводные камни
- Class-level mutable:
class A: cache = []— общий объект для всех экземпляров и наследников. - Глубокая иерархия (3+ уровня) — больно тестировать и менять; предпочитайте композицию.
super().__init__пропущен в одном из mixin-ов — следующая часть MRO не инициализируется.- Множественное наследование с конфликтом MRO — TypeError при определении класса.
- Передача
**kwargsдоobject.__init__— он не принимает аргументов; последний mixin перед object должен «съесть» остатки. - Использовать наследование для повторного использования кода, нарушая LSP — починка одного места ломает другое.
Common mistakes
- Говорить, что методы копируются в дочерний класс.
- Не упоминать MRO.
- Путать наследование интерфейса и наследование реализации.
What the interviewer is testing
- Объясняет поиск атрибутов по MRO.
- Понимает override.
- Знает, когда композиция лучше наследования.