Базовый и дочерний класс
Мы научились создавать классы и работать с атрибутами и методами. Но что делать, когда нескольким классам нужен одинаковый код? Сегодня изучаем наследование — один из главных столпов ООП, который избавляет нас от дублирования и делает программу логичнее.
😫 Жизнь без наследования — боль и копипаст
Представьте: вы разрабатываете программу для автосалона. У вас есть автомобили, мотоциклы, грузовики. Все они имеют кое-что общее: у каждого есть марка, модель, год выпуска и метод для запуска двигателя.
Без наследования вы напишете примерно вот что:
class Car:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def start_engine(self):
print(f"{self.brand} {self.model}: двигатель запущен 🔥")
def get_info(self):
return f"{self.brand} {self.model}, {self.year} г."
class Motorcycle:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def start_engine(self):
print(f"{self.brand} {self.model}: двигатель запущен 🔥")
def get_info(self):
return f"{self.brand} {self.model}, {self.year} г."
class Truck:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def start_engine(self):
print(f"{self.brand} {self.model}: двигатель запущен 🔥")
def get_info(self):
return f"{self.brand} {self.model}, {self.year} г."
🚨 Видите проблему? Одинаковый код повторяется три раза — слово в слово. Если вы захотите изменить метод start_engine (например, добавить звук), придётся лезть в три места. Забудете про одно — получите баг. Это нарушает принцип DRY: Don't Repeat Yourself — «Не повторяй себя».
DRY — это один из фундаментальных принципов хорошего программирования. Идея проста: каждый кусочек знания (логика, данные, поведение) должен существовать в программе в единственном экземпляре. Если что-то встречается дважды — значит, пора думать, как от дублирования избавиться.
Наследование — это именно тот инструмент, который решает эту проблему в ООП.
📚 Разбираемся с терминами
В наследовании участвуют два класса, и у каждого из них есть несколько названий. Они все означают одно и то же — просто разные книги и языки используют разные слова. Запомните эти пары:
«Тот, от кого наследуют»
Это всё синонимы:
- Базовый класс
- Родительский класс
- Суперкласс
- Base class / Parent class / Superclass
«Тот, кто наследует»
Это всё синонимы:
- Дочерний класс
- Подкласс
- Производный класс
- Child class / Subclass / Derived class
В этом курсе мы будем использовать термины «базовый» и «дочерний», но вы должны понимать все варианты — они встречаются в документации и на собеседованиях.
✍️ Синтаксис наследования
В Python наследование записывается очень лаконично. Когда объявляете дочерний класс, в круглых скобках после имени класса пишете имя родителя:
class ДочернийКласс(БазовыйКласс):
# тут код дочернего класса
pass
Это единственное отличие от обычного объявления класса — круглые скобки с именем родителя. Всё остальное работает как обычно.
💡 Ключевое слово pass означает «пустое тело». Python требует, чтобы блок кода после двоеточия не был пустым. Если вам пока нечего добавлять в класс — напишите pass, и Python будет доволен.
Теперь посмотрим на реальный пример. Создадим класс Vehicle («транспортное средство») — это будет наш базовый класс. А потом создадим дочерний класс Car:
class Vehicle:
"""Базовый класс для всех транспортных средств."""
def __init__(self, brand, model, year):
self.brand = brand # марка
self.model = model # модель
self.year = year # год выпуска
def start_engine(self):
print(f"{self.brand} {self.model}: двигатель запущен 🔥")
def get_info(self):
return f"{self.brand} {self.model}, {self.year} г."
class Car(Vehicle):
"""Дочерний класс — легковой автомобиль."""
pass # пока ничего нового не добавляем
Посмотрите: класс Car буквально пуст — в нём только pass. Но это не значит, что он бесполезен! Он унаследовал всё от Vehicle. Убедимся в этом:
my_car = Car("Toyota", "Camry", 2022)
# Все методы родителя доступны в дочернем классе!
print(my_car.get_info()) # Toyota Camry, 2022 г.
my_car.start_engine() # Toyota Camry: двигатель запущен 🔥
# Атрибуты тоже доступны
print(my_car.brand) # Toyota
print(my_car.year) # 2022
🎉 Магия наследования: объект my_car создан из почти пустого класса Car, но имеет доступ ко всем атрибутам и методам, определённым в Vehicle. Ему «по наследству» перешли __init__, start_engine и get_info.
📦 Что именно наследуется?
Дочерний класс получает от родителя всё:
Все методы
Включая __init__, __str__ и другие специальные
Все атрибуты
Те, что заданы в __init__ родителя
Переменные класса
Если они определены в теле родителя
Дочерний класс может использовать родительский функционал как свой собственный — никаких специальных вызовов не нужно. Просто создаёте объект и работаете.
Кроме того, дочерний класс может добавить собственные методы и атрибуты, которых нет в родителе. Например, у машины, в отличие от абстрактного «транспортного средства», может быть количество дверей:
class Car(Vehicle):
"""Легковой автомобиль с дополнительными атрибутами."""
def __init__(self, brand, model, year, doors=4):
# Сначала вызываем __init__ родителя (об этом подробнее в следующем уроке)
super().__init__(brand, model, year)
# Добавляем свой атрибут
self.doors = doors
def describe_doors(self):
print(f"Количество дверей: {self.doors}")
my_car = Car("Honda", "Civic", 2023, doors=4)
print(my_car.get_info()) # Honda Civic, 2023 г. ← метод из Vehicle
my_car.start_engine() # Honda Civic: двигатель запущен 🔥 ← из Vehicle
my_car.describe_doors() # Количество дверей: 4 ← свой метод Car
👀 Заметили super()? Это вызов родительского __init__. Мы разберём его подробно в следующем уроке. Пока просто знайте: он нужен, чтобы атрибуты родителя (brand, model, year) тоже были инициализированы.
🔍 isinstance() и issubclass() — проверяем наследование
Python предоставляет две встроенные функции для работы с наследованием. Они очень полезны, когда нужно разобраться, с каким типом объекта вы работаете.
isinstance(объект, класс) — является ли объект экземпляром класса?
Вы уже знакомы с isinstance — она проверяет, является ли объект экземпляром указанного класса. С наследованием у неё есть важная особенность:
class Vehicle:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
class Car(Vehicle):
pass
my_car = Car("BMW", "X5", 2024)
print(isinstance(my_car, Car)) # True — my_car создан из Car, очевидно
print(isinstance(my_car, Vehicle)) # True — Car наследует Vehicle!
print(isinstance(my_car, str)) # False — это точно не строка :)
💡 Ключевой момент: isinstance(my_car, Vehicle) возвращает True, хотя my_car создан из Car, а не из Vehicle. Дочерний объект считается экземпляром родительского класса. Это называется принципом подстановки: везде, где ожидается Vehicle, можно подставить Car.
issubclass(дочерний, родительский) — является ли один класс подклассом другого?
issubclass работает с классами (не с объектами). Она проверяет, наследует ли один класс другой:
print(issubclass(Car, Vehicle)) # True — Car наследует Vehicle
print(issubclass(Vehicle, Car)) # False — родитель не является подклассом потомка!
print(issubclass(Car, Car)) # True — класс является подклассом самого себя
Обратите внимание на второй пример: наследование — это одностороннее отношение. Car знает о Vehicle, но Vehicle ничего не знает о Car.
🌍 Все классы наследуются от object
В Python есть интересный факт: даже если вы пишете обычный класс без указания родителя, он автоматически наследует встроенный класс object. Это «прародитель» всех классов в Python.
class Vehicle:
pass
# Это то же самое, что написать:
class Vehicle(object):
pass
Поэтому issubclass(Vehicle, object) всегда возвращает True для любого класса.
Чтобы увидеть всю цепочку наследования, Python предоставляет специальный атрибут __mro__ — Method Resolution Order (порядок разрешения методов). Он показывает, в каком порядке Python ищет методы при вызове:
class Vehicle:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
class Car(Vehicle):
pass
# Смотрим цепочку наследования Car
print(Car.__mro__)
# (, , )
# Можно вывести красивее
for cls in Car.__mro__:
print(cls)
#
#
#
🔎 Как читать MRO: Python идёт слева направо. Когда вы вызываете метод на объекте Car, Python сначала ищет его в Car. Не нашёл — ищет в Vehicle. Не нашёл — ищет в object. Именно отсюда у всех объектов есть методы вроде __str__, __repr__, __eq__ — они определены в object.
Также можно использовать метод mro() — он делает то же самое, но возвращает список:
print(Car.mro())
# [, , ]
🚗 Полный пример: Vehicle и Car
Соберём всё вместе. Посмотрим на законченный пример с подробными комментариями и проверим все концепции урока:
class Vehicle:
"""Базовый класс — транспортное средство."""
# Переменная класса (общая для всех транспортных средств)
category = "Транспорт"
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def start_engine(self):
print(f"{self.brand} {self.model}: двигатель запущен 🔥")
def stop_engine(self):
print(f"{self.brand} {self.model}: двигатель остановлен ⛔")
def get_info(self):
return f"{self.brand} {self.model}, {self.year} г."
def __str__(self):
return self.get_info()
class Car(Vehicle):
"""Дочерний класс — легковой автомобиль."""
def __init__(self, brand, model, year, doors=4):
super().__init__(brand, model, year) # инициализируем родителя
self.doors = doors # добавляем свой атрибут
def describe_doors(self):
"""Метод, которого нет в родителе."""
print(f"Дверей: {self.doors}")
# ─── Создаём объекты ───────────────────────────────────────
generic = Vehicle("Generic", "Transport", 2020)
my_car = Car("Tesla", "Model 3", 2024, doors=4)
# ─── Car использует методы Vehicle ────────────────────────
print(my_car.get_info()) # Tesla Model 3, 2024 г.
my_car.start_engine() # Tesla Model 3: двигатель запущен 🔥
my_car.stop_engine() # Tesla Model 3: двигатель остановлен ⛔
# ─── Car также имеет собственные методы ───────────────────
my_car.describe_doors() # Дверей: 4
# ─── Переменная класса тоже наследуется ───────────────────
print(Car.category) # Транспорт
print(my_car.category) # Транспорт
# ─── isinstance и issubclass ──────────────────────────────
print(isinstance(my_car, Car)) # True
print(isinstance(my_car, Vehicle)) # True ← дочерний — это тоже родительский!
print(isinstance(my_car, object)) # True ← всё — это object!
print(isinstance(generic, Car)) # False ← родитель НЕ является дочерним
print(issubclass(Car, Vehicle)) # True
print(issubclass(Vehicle, Car)) # False
print(issubclass(Car, object)) # True ← всегда True для любого класса
# ─── Смотрим цепочку MRO ──────────────────────────────────
print("\nЦепочка наследования Car:")
for cls in Car.__mro__:
print(" →", cls.__name__)
# → Car
# → Vehicle
# → object
Запустите этот код и убедитесь, что понимаете каждую строчку вывода. Это и есть наследование в действии!
✅ Итоги урока
- DRY (Don't Repeat Yourself) — общий код должен быть написан один раз, не копироваться.
- Наследование — механизм, который позволяет дочернему классу получить всё из базового.
- Синтаксис:
class Child(Parent):— имя родителя в скобках. - Дочерний класс наследует все методы и атрибуты родителя и может добавлять свои.
- Суперкласс / родительский / базовый — синонимы. Подкласс / дочерний — тоже синонимы.
isinstance(obj, Parent)→True— дочерний объект считается экземпляром родителя.issubclass(Child, Parent)— проверяет отношение между классами.- Все классы в Python наследуют
object— это вершина иерархии. __mro__показывает полную цепочку поиска методов.
👉 В следующем уроке мы разберём, как правильно переопределять методы родителя и как работает загадочная функция super(). Вы увидите, как дочерний класс может не просто использовать код родителя, но и расширять его.