Объектно-ориентированное программирование

Наследование, super() и переопределение методов

Содержание курса

super(): расширение родительской логики без дублирования

Полное переопределение метода — это выбор «всё или ничего»: родительская реализация полностью вытесняется. Но что если нужно не заменить поведение родителя, а добавить к нему что-то своё? Вот тут появляется super().

super() возвращает прокси-объект, который переадресует вызов метода следующему классу в MRO. В простом случае с одним родителем это означает: «вызови тот же метод из родительского класса». Никакого дублирования кода родителя, никакого жёсткого упоминания имени родительского класса — только делегирование.

Самый распространённый сценарий — расширение __init__. Допустим, Animal уже инициализирует name, а Dog хочет добавить ещё и breed. Без super() придётся либо скопировать логику родителя, либо потерять её:

class Animal:
    def __init__(self, name: str) -> None:
        self.name = name

    def speak(self) -> str:
        return "..."

class Dog(Animal):
    def __init__(self, name: str, breed: str) -> None:
        super().__init__(name)   # 1) инициализируем родительскую часть
        self.breed = breed       # 2) добавляем собственный атрибут

    def speak(self) -> str:
        return "Woof"

Что здесь происходит по шагам:

  1. Dog.__init__ вызывается с name и breed.
  2. super().__init__(name) передаёт name в Animal.__init__, который устанавливает self.name. Это полноценный вызов родительского конструктора — никакой магии, просто делегирование.
  3. После возврата из super().__init__ продолжается инициализация Dog: устанавливается self.breed.
dog = Dog("Rex", "Labrador")
print(dog.name)   # Rex
print(dog.breed)  # Labrador
print(dog.speak()) # Woof

Оба атрибута на месте: name — от родителя, breed — от подкласса.

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

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

class Animal:
    def describe(self) -> str:
        return f"Animal: {self.name}"

class Dog(Animal):
    def __init__(self, name: str, breed: str) -> None:
        super().__init__(name)
        self.breed = breed

    def describe(self) -> str:
        base = super().describe()          # берём строку от родителя
        return f"{base}, breed: {self.breed}"  # дополняем

dog = Dog("Rex", "Labrador")
print(dog.describe())  # Animal: Rex, breed: Labrador

Dog.describe() не переписывает логику Animal.describe() — он её использует и надстраивает сверху. Это и есть принципиальное отличие от полного переопределения: не «вместо», а «поверх».

Технически super() без аргументов работает в Python 3 автоматически: интерпретатор знает, в каком классе и методе он вызван, и подставляет нужные значения сам. Явная форма super(Dog, self).__init__(name) из Python 2 в современном коде не нужна — она только засоряет читаемость.