$ sudo teach IT
МОДУЛЬ 3 · УРОК 1

Базовый и дочерний класс

Мы научились создавать классы и работать с атрибутами и методами. Но что делать, когда нескольким классам нужен одинаковый код? Сегодня изучаем наследование — один из главных столпов ООП, который избавляет нас от дублирования и делает программу логичнее.

⏱ ~30 минут 🎓 Для новичков 🐍 Python 3

😫 Жизнь без наследования — боль и копипаст

Представьте: вы разрабатываете программу для автосалона. У вас есть автомобили, мотоциклы, грузовики. Все они имеют кое-что общее: у каждого есть марка, модель, год выпуска и метод для запуска двигателя.

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

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(). Вы увидите, как дочерний класс может не просто использовать код родителя, но и расширять его.