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

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

Содержание

  1. Что такое наследование
  2. Наследование от object
  3. Родительские и дочерние классы
  4. Что наследуется, а что нет
  5. Переопределение методов
  6. super()
  7. MRO и множественное наследование
  8. Практика

Что такое наследование

Наследование - это механизм ООП, при котором один класс получает методы и атрибуты другого класса.

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

Что такое классы, атрибуты и методы — мы разбирали в 03.04 - ООП Классы.

Главная польза наследования - не переписывать одинаковый код в нескольких классах.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
    def get_info(self):
        return f"{self.name}: {self.salary} руб."
 
class Developer(Employee):
    def write_code(self):
        return f"{self.name} пишет код"
 
class Tester(Employee):
    def test_feature(self):
        return f"{self.name} проверяет функциональность"

Developer и Tester получают __init__ и get_info() от Employee, а свои особые действия описывают отдельно.


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

Даже пустой класс в Python не совсем пустой. Он автоматически наследуется от базового класса object.

class MyClass:
    pass
 
obj = MyClass()
print(dir(obj))

В списке появятся методы вроде __init__, __str__, __repr__, __eq__. Они пришли от object.

Эти две записи равнозначны:

class MyClass:
    pass
 
class MyClass(object):
    pass

Это значит, что наследование используется в Python постоянно, даже когда мы явно об этом не думаем.


Родительские и дочерние классы

Без наследования часто появляется повторение:

class CourseStudent:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
    def show_progress(self):
        print(f"{self.name}: {self.score} баллов")
 
class Mentor:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
    def show_progress(self):
        print(f"{self.name}: {self.score} баллов")

Общий код можно вынести в родительский класс:

class CourseMember:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
    def show_progress(self):
        print(f"{self.name}: {self.score} баллов")
 
class CourseStudent(CourseMember):
    def submit_homework(self):
        print(f"{self.name} отправил домашнее задание")
 
class Mentor(CourseMember):
    def review_homework(self):
        print(f"{self.name} проверяет домашнее задание")

Теперь CourseStudent и Mentor используют общую инициализацию и метод show_progress().

student = CourseStudent("Ирина", 84)
mentor = Mentor("Олег", 100)
 
student.show_progress()
student.submit_homework()
 
mentor.show_progress()
mentor.review_homework()

Что наследуется, а что нет

Наследуются:

  • методы родительского класса;
  • конструктор __init__, если в дочернем классе нет своего;
  • атрибуты класса;
  • магические методы, если они доступны по цепочке наследования.

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

class BaseUser:
    role = "user"  # атрибут класса
 
    def __init__(self, name):
        self.name = name  # атрибут объекта
 
class Admin(BaseUser):
    pass
 
admin = Admin("Марина")
print(admin.role)
print(admin.name)

role доступен как атрибут класса, а name создается для конкретного объекта при выполнении __init__.

Приватные атрибуты с двойным подчеркиванием (__secret) напрямую в дочерних классах не используются из-за механизма name mangling. Для наследуемой внутренней логики чаще применяют одно подчеркивание: _protected_value.


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

Дочерний класс может заменить метод родителя своей версией. Это называется переопределением.

class Notification:
    def send(self):
        return "Отправляем обычное уведомление"
 
class EmailNotification(Notification):
    def send(self):
        return "Отправляем уведомление по email"
 
class SmsNotification(Notification):
    def send(self):
        return "Отправляем уведомление по SMS"
 
print(EmailNotification().send())
print(SmsNotification().send())

Название метода одинаковое, но поведение у объектов разное. Это уже подводит нас к полиморфизму.


super()

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

class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
 
    def get_info(self):
        return f"{self.brand} {self.model}"
 
class Car(Vehicle):
    def __init__(self, brand, model, doors):
        super().__init__(brand, model)
        self.doors = doors
 
    def get_info(self):
        base_info = super().get_info()
        return f"{base_info}, дверей: {self.doors}"
 
car = Car("Skoda", "Octavia", 4)
print(car.get_info())

super().__init__(brand, model) вызывает родительский конструктор, чтобы не повторять присваивание brand и model.

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

Используем super(), когда:

  • нужно сохранить логику родителя и добавить новую;
  • дочерний класс имеет дополнительные атрибуты;
  • есть цепочка классов, где каждый слой должен выполнить свою часть работы;
  • важно не дублировать код.

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


MRO и множественное наследование

MRO (Method Resolution Order) - порядок, в котором Python ищет методы по цепочке наследования.

class A:
    def method(self):
        print("A")
 
class B(A):
    def method(self):
        print("B")
        super().method()
 
class C(A):
    def method(self):
        print("C")
        super().method()
 
class D(B, C):
    def method(self):
        print("D")
        super().method()
 
obj = D()
obj.method()
print(D.__mro__)

Python не просто идет к первому родителю. Он строит порядок поиска, чтобы каждый класс в цепочке вызывался корректно. Поэтому в сложных иерархиях super() безопаснее прямого вызова вроде A.method(self).


Практика

  • Задание 1: пустое наследование

    Создайте пустой класс Device и класс Laptop, который наследуется от Device. Создайте объект Laptop и выведите dir().

    Проверьте, что даже у пустого дочернего класса есть методы, полученные через object.

  • Задание 2: неправильное количество аргументов

    Создайте классы и специально вызовите ошибки.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
     
    class Student(Person):
        pass
     
    # Попробуйте:
    # Person("Олег")
    # Student()
    # Student("Марина", 20, "Python")

    Прочитайте текст ошибок и обратите внимание, какой __init__ Python пытается вызвать.

  • Задание 3: общие методы в родителе

    Создайте родительский класс Shape с атрибутом color и методами get_color() и describe(). Создайте дочерний класс Circle, который наследует эти методы.

    Добавьте объект Circle и проверьте, что методы доступны без повторного объявления.

  • Задание 4: исправление дублирования через super()

    В коде ниже дочерние классы повторяют логику родителя. Перепишите их через super().__init__().

    class Vehicle:
        def __init__(self, brand, model, year):
            self.brand = brand
            self.model = model
            self.year = year
            self.vehicle_id = f"{brand}_{model}_{year}"
     
    class Car(Vehicle):
        def __init__(self, brand, model, year, doors):
            self.brand = brand
            self.model = model
            self.year = year
            self.vehicle_id = f"{brand}_{model}_{year}"
            self.doors = doors
     
    class Scooter(Vehicle):
        def __init__(self, brand, model, year, battery_capacity):
            self.brand = brand
            self.model = model
            self.year = year
            self.vehicle_id = f"{brand}_{model}_{year}"
            self.battery_capacity = battery_capacity
  • Задание 5: расширение метода

    Создайте класс Student с методом complete_task(task_name, points). Затем создайте класс HonorStudent, который вызывает родительский метод через super() и добавляет бонус 10%.

    Проверьте на значениях: task_name="ООП практика", points=120.

  • Задание 6: цепочка валидации

    Создайте классы BaseValidator, LengthValidator, FormatValidator.

    • BaseValidator.validate(data) очищает список ошибок и возвращает True;
    • LengthValidator проверяет минимальную длину;
    • FormatValidator проверяет наличие обязательного символа, например @.

    Каждый дочерний класс должен использовать super().validate(data).

  • Задание 7: исследование MRO

    Повторите пример с классами A, B, C, D, поменяйте порядок наследования в D(B, C) на D(C, B) и сравните вывод __mro__.

    Ответьте себе:

    1. Почему порядок вызовов поменялся?
    2. Какую роль сыграл super()?
    3. Что изменится, если вызвать родительский метод напрямую?
  • Задание 8: проектная задача

    Создайте цепочку обработки заказа:

    • OrderProcessor - базовая обработка заказа;
    • PaymentProcessor - проверка оплаты;
    • InventoryProcessor - проверка наличия товаров;
    • ShippingProcessor - расчет доставки.

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

    Используйте тестовый заказ:

    order = {
        "id": "A-204",
        "items": ["keyboard", "monitor"],
        "amount": 18600,
        "address": "Казань, ул. Тестовая, 12"
    }

Короткий итог

Наследование позволяет строить иерархии классов и переиспользовать общую логику. Родительский класс хранит общее поведение, дочерний добавляет детали. super() помогает расширять родительские методы без копирования, а MRO определяет, в каком порядке Python ищет нужный метод.


⬅️ Назад: 03.04 - ООП Классы | Далее: 03.06 - Методы классов ➡️ Модуль: 03 - MOC