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

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

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

В прошлом уроке мы разбирали композицию: один объект хранит другой как атрибут и делегирует ему работу. Это отношение «имеет» — has-a. Теперь переходим к другому механизму связи классов: наследованию, которое выражает отношение «является» — is-a. Это принципиально другая идея, и перепутать их — одна из самых частых ошибок в проектировании.

Базовый класс и подкласс: механика наследования

Наследование объявляется в одну строку — имя родителя указывается в скобках при определении класса:

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

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

class Dog(Animal):
    pass

Dog здесь ничего не определяет сам. Но это уже полноценный класс: он автоматически получает __init__ и speak от Animal. Можно создать экземпляр и вызвать метод без какого-либо дополнительного кода:

dog = Dog("Rex")
print(dog.name)    # Rex
print(dog.speak()) # ...

Python ищет атрибут или метод сначала в самом объекте, потом в его классе (Dog), потом поднимается по цепочке родителей. Это и есть MRO — Method Resolution Order. В простом случае с одним родителем цепочка линейная: Dog → Animal → object.

Проверить, является ли объект экземпляром родительского класса, можно через isinstance:

print(isinstance(dog, Dog))    # True
print(isinstance(dog, Animal)) # True

Оба вызова возвращают True — потому что Dog действительно является разновидностью Animal. Именно это слово «является» — ключевое.

Критерий is-a. Наследование имеет смысл только тогда, когда можно честно сказать: «подкласс является разновидностью базового класса». Dog является животным — это реальное is-a отношение. Но если между классами связь другого рода — один использует другой, один хранит другой — наследование здесь не на месте.

Проверка простая: подставьте оба класса в фразу «X является Y». Если фраза звучит нелепо или натянуто — скорее всего, нужна композиция, а не наследование. Например, «Движок является Автомобилем» — нет. «Автомобиль имеет Движок» — да. Значит, Engine должен быть атрибутом Car, а не его родителем.

Этот критерий — не философия, а практический фильтр. Неправильно применённое наследование создаёт классы, которые трудно менять: изменение родителя ломает всех детей, а замена поведения требует переписывать цепочку. Когда is-a соблюдается — наследование работает на вас. Когда нет — оно становится источником хрупкости.