Наследование
В этой теме разбираем наследование: как один класс может получать поведение другого, как расширять родительскую логику и почему super() часто лучше копирования кода.
Содержание
- Что такое наследование
- Наследование от
object - Родительские и дочерние классы
- Что наследуется, а что нет
- Переопределение методов
super()- MRO и множественное наследование
- Практика
Что такое наследование
Наследование - это механизм ООП, при котором один класс получает методы и атрибуты другого класса.
Класс, от которого наследуются, называют родительским, базовым или суперклассом. Класс, который наследует поведение, называют дочерним или подклассом.
Что такое классы, атрибуты и методы — мы разбирали в 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__.Ответьте себе:
- Почему порядок вызовов поменялся?
- Какую роль сыграл
super()? - Что изменится, если вызвать родительский метод напрямую?
-
Задание 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