PythonJuniorTechnical

Как работает наследование в Python?

Класс наследует атрибуты и методы от базового. Поиск атрибута идёт по MRO (cls.__mro__), super() вызывает следующий по MRO. Поддерживается множественное наследование (C3-линеаризация). Часто лучше композиция: дешевле менять и тестировать.

Базовая механика

Класс-наследник получает доступ ко всем атрибутам/методам предков. Когда происходит обращение obj.attr:

  1. Сначала Python ищет attr в obj.__dict__ (instance attribute) — кроме data descriptors.
  2. Затем по type(obj).__mro__ (Method Resolution Order) обходит классы слева направо: own class → parents → ... → object.
  3. Если найден descriptor (например, function, property, classmethod) — применяется descriptor protocol, метод биндится к экземпляру.
  4. Если не найдено — вызывается __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 не нарушается: Dog is Animal.
  • Композиция — когда «has-a» или просто шарим поведение: UserService has UserRepository.
  • 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.
  • Знает, когда композиция лучше наследования.

Sources

Related topics