📖 Теория: наследование и полиморфизм

⚡ Кратко

Наследование передаёт поля и методы от родителя потомку. Полиморфизм позволяет одинаковому вызову работать по-разному благодаря переопределению методов. super() вызывает родительскую версию метода. Все классы наследуют от object. isinstance проверяет объект, issubclass — класс.

  • class Child(Parent): — базовый синтаксис наследования.
  • Переопределение метода: заново определить метод в дочернем классе.
  • В Python нет перегрузки по количеству аргументов: последнее определение заменяет предыдущее.
  • super() находит ближайшего родителя и учитывает порядок разрешения методов.

Четыре принципа ООП

Объектно-ориентированное программирование строится на четырёх принципах. Они помогают проектировать код так, чтобы его было легче расширять и поддерживать.

ПринципКраткое описание
Наследование Один класс может унаследовать свойства и поведение другого. Позволяет избегать дублирования кода и переиспользовать логику.
Инкапсуляция Объект скрывает внутреннее устройство и предоставляет только нужный интерфейс. Помогает защищать данные и контролировать доступ к ним.
Полиморфизм Одинаковый интерфейс может вести себя по-разному в зависимости от типа объекта. Позволяет использовать одинаковый код для разных классов.
Абстракция Выделение общих характеристик и поведения, чтобы задать интерфейс для будущих реализаций в наследниках.

Знание принципов упрощает понимание механизмов наследования, переопределения и взаимодействия объектов, а также помогает писать гибкий и расширяемый код.

Наследование

Наследование — это механизм, который позволяет одному классу перенять свойства и поведение другого. Класс, от которого наследуются, называется родительским, базовым или суперклассом. Класс, который наследует, называется дочерним, производным или подклассом.

class Parent:
    # родительский класс
    ...


class Child(Parent):
    # дочерний класс, наследует всё от Parent
    ...

Зачем нужно наследование:

  • Избежать дублирования кода, выделив общее поведение в один базовый класс.
  • Удобно расширять существующую логику без переписывания старых классов.
  • Работать с разными объектами одинаково, если они наследуются от общего предка.
class Employee:
    def __init__(self, name: str) -> None:
        self.name = name

    def work(self) -> None:
        print(f"{self.name} is working...")


class Programmer(Employee):
    pass


class Manager(Employee):
    pass


programmer = Programmer("Alice")
manager = Manager("Bob")

programmer.work()
manager.work()

Что происходит: Employee хранит имя сотрудника и умеет работать. Programmer и Manager наследуют поле name и метод work(), поэтому могут использовать их без повторного определения.

💡 На заметку: дочерний класс наследует все поля и методы родителя, но может добавлять свои и переопределять унаследованное поведение.

Полиморфизм

Полиморфизм (от греч. «много форм») — это возможность использовать один и тот же интерфейс для разных типов объектов. Разные классы могут реализовывать один и тот же метод по-своему, а вызывающий код работает с ними одинаково.

Зачем нужен полиморфизм:

  • Позволяет писать универсальный код, не заботясь о типе объекта.
  • Упрощает расширение программы — можно добавлять новые типы, не меняя старую логику.
  • Делает код гибким и читаемым.

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

Когда дочерний класс заново определяет метод, унаследованный от родителя, это называется overriding. При вызове такого метода Python использует реализацию из дочернего класса.

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

    def work(self) -> None:
        print(f"{self.name} is working...")


class Programmer(Employee):
    def work(self) -> None:
        print(f"{self.name} writing code...")


class Manager(Employee):
    def work(self) -> None:
        print(f"{self.name} managing team...")


staff = [Programmer("Alice"), Manager("Bob"), Programmer("Bill")]

for person in staff:
    person.work()

Что происходит: у всех сотрудников есть метод work(), но он реализован по-разному. В списке staff хранятся объекты разных классов, и Python автоматически вызывает нужную версию метода в зависимости от типа объекта. Это и есть полиморфизм.

Перегрузка методов в Python

Перегрузка (overloading) — это когда один и тот же метод ведёт себя по-разному в зависимости от переданных аргументов. В Python нет классической перегрузки по количеству аргументов, как в Java или C++.

class Math:
    def add(self, a: int, b: int) -> int:
        return a + b

    def add(self, a: int, b: int, c: int) -> int:
        return a + b + c


m = Math()
print(m.add(1, 2, 3))  # Работает
print(m.add(1, 2))     # TypeError!

Вторая версия add() просто заменяет первую — Python запоминает только последнее определение метода.

Функция super()

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

Неправильный способ: дублирование кода

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


class Programmer(Employee):
    def __init__(self, name: str, language: str) -> None:
        self.name = name  # повторяем то, что делает родитель
        self.language = language

Такой код нарушает принцип DRY (Do not Repeat Yourself): если поведение Employee.__init__ изменится, Programmer об этом не узнает.

Альтернативный неправильный способ: явный вызов родителя

class Programmer(Employee):
    def __init__(self, name: str, language: str) -> None:
        Employee.__init__(self, name)  # привязано к имени класса
        self.language = language

Этот способ работает, но считается плохой практикой: он жёстко привязан к имени родительского класса, может некорректно работать при изменении иерархии и не учитывает порядок разрешения методов.

Правильный способ: super()

class Programmer(Employee):
    def __init__(self, name: str, language: str) -> None:
        super().__init__(name)
        self.language = language


class Manager(Employee):
    def __init__(self, name: str, department: str) -> None:
        super().__init__(name)
        self.department = department


p = Programmer("Alice", "Python")
print(p.name)
print(p.language)

super() в цепочке наследования

Когда уровней наследования несколько, super() позволяет вызывать инициализацию каждого уровня без жёсткой привязки к имени родительского класса.

class Person:
    def __init__(self, name: str) -> None:
        self.name = name
        print(f"Init Person: {self.name}")


class Employee(Person):
    def work(self) -> None:
        print(f"{self.name} is working...")


class Manager(Employee):
    def __init__(self, name: str, department: str) -> None:
        super().__init__(name)
        self.department = department
        print(f"Init Manager: {self.name} manages {self.department}")


m = Manager("Alice", "Development")

super() в обычных методах

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

class Employee:
    def work(self) -> None:
        print("Employee is doing general tasks.")


class Programmer(Employee):
    def work(self) -> None:
        super().work()
        print("Programmer is writing code.")


class Manager(Employee):
    def work(self) -> None:
        super().work()
        print("Manager is holding a meeting.")


staff = [Programmer(), Manager()]
for person in staff:
    person.work()
    print()
💡 На заметку: super() автоматически находит метод в ближайшем родителе, избегает дублирования и корректно работает при изменении иерархии классов.

Наследование от object

В Python все классы неявно наследуются от встроенного класса object, даже если родитель не указан. object — это корневой класс всей иерархии классов в Python. Он предоставляет базовые методы, такие как __str__(), __init__() и другие.

class Book:
    pass


# Python воспринимает это как:
# class Book(object):
#     pass

b = Book()
print(b.__str__())  # Вызов метода __str__ напрямую
print(b)            # print() тоже вызывает __str__

Метод __str__() присутствует в классе без собственной реализации, потому что он унаследован от object. Все классы получают единое поведение по умолчанию и становятся совместимыми со встроенными механизмами Python.

Функции isinstance и issubclass

Python предоставляет удобные встроенные функции для проверки типов и иерархии классов, что особенно полезно при работе с наследованием и полиморфизмом.

isinstance

isinstance(obj, some_class) проверяет, является ли объект экземпляром указанного класса или его наследника. Если да — возвращает True, иначе — False. Вторым аргументом можно передать кортеж классов.

class Employee:
    pass


class Programmer(Employee):
    pass


class Manager(Employee):
    pass


e = Employee()
p = Programmer()
m = Manager()

print(isinstance(p, Programmer))  # True — экземпляр класса
print(isinstance(p, Employee))    # True — экземпляр наследника
print(isinstance(p, Manager))     # False — другая ветка иерархии
print(isinstance(p, object))      # True — все классы наследуют от object
print(isinstance("hello", (str, int)))  # True — строка принадлежит к одному из классов

issubclass

issubclass(class_a, class_b) проверяет, является ли class_a подклассом class_b напрямую или через цепочку наследования. Вторым аргументом тоже можно передать кортеж.

class BackendDeveloper(Programmer):
    pass


print(issubclass(Programmer, Employee))         # True — прямой потомок
print(issubclass(BackendDeveloper, Programmer)) # True — прямой потомок
print(issubclass(BackendDeveloper, Employee))   # True — потомок через цепочку
print(issubclass(Manager, Programmer))          # False — разные ветки
print(issubclass(Employee, object))             # True — все классы наследуют от object
⚠️ Важно: isinstance принимает объект, а issubclass — класс. Передача объекта в issubclass вызовет TypeError.
Проверить по документации: подробнее о super(), isinstance() и issubclass() см. Built-in Functions. Описание объектной модели — в разделе Data model.
Проверить по документации: аннотации типов в примерах выходят за рамки исходного материала. Подробнее см. typing и PEP 484.