$ sudo teach IT
🐍 Урок 3.2 · Наследование

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

Ты уже умеешь наследовать классы. Теперь узнаешь, как дочерний класс может переписать поведение родителя — и почему это один из самых мощных инструментов ООП.

🤔 Что значит «переопределить» метод?

Представь: есть родительский класс Animal (Животное). У него есть метод speak(), который возвращает просто строку «Звук». Логично — мы не знаем заранее, какое именно животное говорит.

Теперь создаём дочерний класс Dog (Собака). Собака говорит «Гав», а не «Звук». Значит нам нужно заменить метод speak() на свою версию.

Переопределение метода — это когда дочерний класс создаёт метод с тем же именем, что и в родительском классе. Python будет использовать версию дочернего класса, а не родительского.

💻 Смотрим на код

Создадим класс Animal и два дочерних класса — Dog и Cat:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Звук"


class Dog(Animal):
    def speak(self):          # переопределяем метод speak()
        return "Гав!"


class Cat(Animal):
    def speak(self):          # переопределяем метод speak()
        return "Мяу!"


dog = Dog("Бобик")
cat = Cat("Мурка")
animal = Animal("Неизвестно")

print(dog.speak())     # Гав!
print(cat.speak())     # Мяу!
print(animal.speak())  # Звук

Обрати внимание: метод называется одинаково — speak() — но каждый класс выдаёт своё. Это и есть переопределение.

🔍 Как Python решает, чей метод вызвать?

Когда ты пишешь dog.speak(), Python не гадает — он следует чёткому правилу. Называется это MRO — Method Resolution Order (порядок разрешения методов). Звучит страшно, но суть простая:

1️⃣
Сначала ищем метод в классе самого объекта
Если объект — Dog, смотрим в Dog
2️⃣
Если нет — смотрим в родительском классе
Поднимаемся вверх по цепочке наследования
3️⃣
Используем первый найденный вариант
Дальше не ищем — нашли и взяли

Посмотрим на это наглядно:

class Animal:
    def speak(self):
        return "Звук"

class Dog(Animal):
    def speak(self):
        return "Гав!"

dog = Dog("Бобик")

# Python проверяет: есть ли speak() в классе Dog? → ДА → вызываем его
# До Animal даже не доходит!
print(dog.speak())  # Гав!

# Можно явно посмотреть порядок поиска методов:
print(Dog.__mro__)
# (, , )
Вывод: Дочерний класс всегда «побеждает». Если метод определён и в родителе, и в потомке — Python возьмёт версию потомка.

🎯 Когда переопределение реально нужно?

Суть идеи такая: родительский класс задаёт шаблон — говорит «у каждого животного есть метод speak()». А дочерний класс заполняет этот шаблон конкретикой — собака говорит «Гав», кошка — «Мяу».

Ещё один пример из жизни — геометрические фигуры:

class Shape:
    def area(self):
        return 0  # базовая реализация — ничего не знаем о конкретной фигуре


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height  # конкретная формула для прямоугольника


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2  # конкретная формула для круга


rect = Rectangle(4, 5)
circle = Circle(3)

print(rect.area())    # 20
print(circle.area())  # 28.27...

Видишь паттерн? Один и тот же вызов .area() — разный результат в зависимости от класса объекта. Это называется полиморфизм, и переопределение методов — главный инструмент для его реализации.

🔄 Один вызов — разный результат

Самый крутой момент: можно положить разные объекты в список и вызвать один метод — каждый ответит по-своему:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Звук"


class Dog(Animal):
    def speak(self):
        return "Гав!"


class Cat(Animal):
    def speak(self):
        return "Мяу!"


class Duck(Animal):
    def speak(self):
        return "Кря!"


# Список разных животных
animals = [Dog("Бобик"), Cat("Мурка"), Duck("Кряква"), Dog("Рекс")]

for animal in animals:
    print(f"{animal.name} говорит: {animal.speak()}")

# Бобик говорит: Гав!
# Мурка говорит: Мяу!
# Кряква говорит: Кря!
# Рекс говорит: Гав!
Мы не знаем заранее, какой именно объект в списке — но просто вызываем .speak(), и каждый отвечает правильно. Это и есть сила переопределения методов.

⚠️ Расширение поведения — и проблема дублирования

Иногда нужно не заменить родительский метод полностью, а добавить к нему что-то. Например, собака лает, но ещё и виляет хвостом. Без специальных инструментов это выглядит так:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} издаёт звук"


class Dog(Animal):
    def speak(self):
        # Хотим добавить к родительскому поведению своё
        # Но super() мы ещё не знаем, поэтому копируем логику вручную:
        base_sound = f"{self.name} издаёт звук"   # ← дублируем код из Animal!
        return base_sound + " и виляет хвостом 🐕"


dog = Dog("Бобик")
print(dog.speak())
# Бобик издаёт звук и виляет хвостом 🐕
🚨 Проблема: строка f"{self.name} издаёт звук" написана дважды — в Animal и в Dog. Если завтра мы изменим эту строку в Animal, в Dog она останется старой. Это называется дублирование кода — плохая практика.

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

✅ Итоги урока

  • Переопределение — создаём метод с тем же именем в дочернем классе
  • Python ищет метод сначала в дочернем классе, потом в родительском
  • Родитель задаёт шаблон, потомок — конкретную реализацию
  • Один вызов у разных объектов → разный результат = полиморфизм
  • Расширение без super() ведёт к дублированию кода — в следующем уроке исправим это