Вступление: Зачем нужно наследование?

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

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

class Goblin:
    def __init__(self, health=50, damage=5):
        self.health = health
        self.damage = damage

    def attack(self, target):
        target.take_damage(self.damage)
        print(f"Гоблин атакует и наносит {self.damage} урона!")

    def take_damage(self, amount):
        self.health -= amount
        print(f"У гоблина осталось {self.health} здоровья.")

Отлично. Теперь для дракона. Он похож, но сильнее и ещё дышит огнём. Ладно, можно скопировать код Goblin, поменять название класса на Dragon, увеличить здоровье и урон... Стоп. А если у нас будет 50 видов врагов? Мы будем 50 раз копировать один и тот же код для attack и take_damage?

Это плохая идея. Во-первых, это утомительно. Во-вторых, если мы захотим изменить логику получения урона (например, добавить проверку на броню), нам придётся вносить правки в 50 разных местах. Это прямой путь к ошибкам и головной боли.

Здесь на помощь приходит один из трёх китов объектно-ориентированного программирования — наследование.

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

Проще говоря, вместо того чтобы говорить: «Гоблин — это что-то, у чего есть здоровье и атака. Дракон — это что-то, у чего есть здоровье и атака», мы говорим: «Сначала создадим общий шаблон "Враг" с базовыми характеристиками. А гоблин и дракон — это разновидности этого врага, которые наследуют всё от него и добавляют что-то своё».

Этот подход позволяет:

  • Избежать дублирования кода (принцип DRY — Don't Repeat Yourself). Общая логика пишется один раз в родительском классе.

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

  • Легко расширять программу. Добавить нового врага — Орка — станет гораздо проще.

В этой статье мы на практике разберёмся, как работает наследование в Python. Мы создадим базовый класс Enemy, а затем, используя его как фундамент, построим более сложных и интересных противников. Давайте начинать

2. Создаём «чертёж» — базовый класс Enemy

Итак, мы договорились не копировать код. Вместо этого мы создадим один-единственный базовый класс (его ещё называют родительским или суперклассом), который будет служить «чертежом» для всех врагов в нашей игре. В нём мы опишем все общие свойства и действия, которые присущи любому противнику.

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

  1. Иметь атрибуты: имя, здоровье и силу урона.

  2. Уметь атаковать: наносить урон цели.

  3. Уметь получать урон: терять здоровье.

  4. Сообщать, жив ли он: проверять, осталось ли у него здоровье.

Теперь переведём это на язык Python.

class Enemy:
    # Метод-конструктор, который вызывается при создании нового объекта
    def __init__(self, name, health, damage):
        self.name = name
        self.health = health
        self.damage = damage
        print(f"Враг '{self.name}' с {self.health} HP появился в мире!")

    # Метод для атаки на какую-либо цель (target)
    def attack(self, target):
        print(f"'{self.name}' атакует {target.name} и наносит {self.damage} урона!")
        target.take_damage(self.damage)

    # Метод для получения урона
    def take_damage(self, amount):
        self.health -= amount
        if self.health <= 0:
            self.health = 0 # Здоровье не может быть отрицательным
            print(f"'{self.name}' получил {amount} урона и был побежден.")
        else:
            print(f"У '{self.name}' осталось {self.health} HP.")

    # Метод для проверки, жив ли враг
    def is_alive(self):
        return self.health > 0

Давайте разберём этот код по частям:

  • class Enemy: — так мы объявляем о создании нового класса.

  • __init__(self, name, health, damage) — это конструктор. Он автоматически вызывается, когда мы создаём новый экземпляр врага (например, enemy1 = Enemy(...)). self — это специальный параметр, который ссылается на сам создаваемый объект. Именно через self мы присваиваем ему атрибуты: self.name = name и так далее.

  • attack(self, target) — обычный метод. Обратите внимание, что он принимает не только self, но и target — объект, который мы будем атаковать. Мы предполагаем, что у этого target тоже есть метод take_damage.

  • take_damage(self, amount) — этот метод уменьшает здоровье (self.health) на величину amount. Мы также добавили проверку, чтобы здоровье не уходило в минус.

  • is_alive(self) — простой и полезный метод, который возвращает True, если здоровье больше нуля, и False в противном случае.

Проверяем наш чертёж в деле

Класс — это лишь описание, чертёж. Чтобы им воспользоваться, нужно создать объект (или экземпляр) этого класса. Давайте создадим простого врага и героя, чтобы посмотреть, как всё работает. Для этого нам понадобится простенький класс Hero.

# Вспомогательный класс для демонстрации
class Hero:
    def __init__(self, name="Герой", health=100):
        self.name = name
        self.health = health

    def take_damage(self, amount):
        self.health -= amount
        print(f"У '{self.name}' осталось {self.health} здоровья.\n")

# --- Теперь используем наш класс Enemy ---

# Создаем объекты
hero = Hero()
generic_enemy = Enemy(name="Обычный скелет", health=40, damage=10)

# Проверяем, жив ли враг
print(f"Статус врага: {'Жив' if generic_enemy.is_alive() else 'Мертв'}")

# Враг атакует героя
generic_enemy.attack(hero)

# Герой наносит ответный удар
print(f"'{hero.name}' наносит ответный удар!")
generic_enemy.take_damage(25)

# Снова проверяем статус
print(f"Статус врага: {'Жив' if generic_enemy.is_alive() else 'Мертв'}")

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

Отлично! Мы создали надёжный фундамент. Теперь у нас есть универсальный шаблон Enemy, на основе которого мы можем строить более специализированные и интересные классы врагов. В следующем разделе мы создадим наш первый дочерний класс — Goblin.

3. Первый наследник — класс Goblin

Мы создали универсальный класс Enemy. Теперь давайте создадим первого конкретного врага — Гоблина. Гоблин — это типичный враг: у него есть здоровье, он умеет атаковать. По сути, он полностью подпадает под описание нашего базового класса.

Именно здесь наследование показывает свою силу. Чтобы создать класс Goblin, нам не нужно копировать код из Enemy. Мы просто говорим Python: «Goblin — это разновидность Enemy».

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

# Наш базовый класс из предыдущего шага
class Enemy:
    def __init__(self, name, health, damage):
        self.name = name
        self.health = health
        self.damage = damage
        print(f"Враг '{self.name}' с {self.health} HP появился в мире!")

    def attack(self, target):
        print(f"'{self.name}' атакует {target.name} и наносит {self.damage} урона!")
        target.take_damage(self.damage)

    def take_damage(self, amount):
        self.health -= amount
        if self.health <= 0:
            self.health = 0
            print(f"'{self.name}' получил {amount} урона и был побежден.")
        else:
            print(f"У '{self.name}' осталось {self.health} HP.")

    def is_alive(self):
        return self.health > 0

# А вот и наш первый наследник!
class Goblin(Enemy):
    pass # Ключевое слово pass означает, что мы пока не добавляем ничего нового

И это всё! Невероятно, правда?

Несмотря на то, что тело класса Goblin совершенно пустое, он уже унаследовал всё от Enemy: конструктор __init__, методы attack(), take_damage() и is_alive().

Давайте это проверим. Создадим объект класса Goblin и посмотрим, что он умеет.

# (Код классов Enemy и Goblin должен быть определён выше)

# Вспомогательный класс Hero
class Hero:
    def __init__(self, name="Герой", health=100):
        self.name = name
        self.health = health
    def take_damage(self, amount):
        self.health -= amount
        print(f"У '{self.name}' осталось {self.health} здоровья.\n")


# Создаём объект класса Goblin
hero = Hero()
goblin = Goblin(name="Вредный гоблин", health=30, damage=5)

# Проверяем, что он унаследовал методы родителя
goblin.attack(hero)
print(f"Жив ли гоблин? {'Да' if goblin.is_alive() else 'Нет'}")

Как видите, мы без проблем создали гоблина, передав в конструктор его параметры, и вызвали метод attack(). Хотя мы не писали ни строчки кода внутри Goblin, он работает как полноценный враг. В этом и заключается магия наследования — повторное использование кода.

Переопределение методов (Method Overriding)

Но что, если мы хотим, чтобы гоблин атаковал как-то по-особенному? Например, мы хотим, чтобы сообщение о его атаке было более характерным. Нам не нужно менять базовый класс Enemy, ведь его атака должна оставаться универсальной.

Вместо этого мы можем переопределить родительский метод в дочернем классе. Это значит, что мы создаём в классе Goblin метод с точно таким же названием, как у родителя (attack), но с другой реализацией.

class Goblin(Enemy):
    # Переопределяем метод attack
    def attack(self, target):
        print(f"'{self.name}' трусливо тычет в {target.name} своим ржавым кинжалом, нанося {self.damage} урона!")
        target.take_damage(self.damage) # Логика нанесения урона остается той же

Что произошло? Теперь, когда мы вызовем метод attack() у объекта класса Goblin, Python сначала поищет этот метод в самом классе Goblin. Он его найдёт и выполнит. Если бы он его там не нашёл, он бы поднялся «вверх по цепочке» к родительскому классу Enemy и выполнил бы метод оттуда.

Давайте сравним поведение обычного Enemy и нашего нового Goblin:

# Создаем объекты
generic_enemy = Enemy("Солдат", 100, 15)
goblin = Goblin("Вредный гоблин", 30, 5)
hero = Hero()

# Сравниваем их атаки
generic_enemy.attack(hero)
goblin.attack(hero)

Вывод будет таким:

'Солдат' атакует Герой и наносит 15 урона!
У 'Герой' осталось 85 здоровья.

'Вредный гоблин' трусливо тычет в Герой своим ржавым кинжалом, нанося 5 урона!
У 'Герой' осталось 80 здоровья.

Результат налицо! Мы сохранили общую логику (нанесение урона), но изменили «косметическую» часть, придав гоблину индивидуальность. Это мощный инструмент, который позволяет настраивать поведение дочерних классов, не трогая родительский.

Мы научились не только наследовать, но и изменять унаследованное. А что, если мы хотим не заменить, а дополнить функциональность родителя? Об этом мы поговорим в следующем разделе на примере создания Дракона.

4. Расширяем функциональность — класс Dragon и super()

Мы научились создавать наследников и полностью переопределять их методы. Гоблин — хороший пример, но он довольно простой. Давайте возьмёмся за врага посерьёзнее — Дракона.

Дракон — это тоже враг, но у него есть уникальные особенности. Мы хотим не просто заменить поведение родителя, а дополнить или расширить его.

Вот наши требования к Дракону:

  1. Он должен иметь все базовые атрибуты Enemy (name, health, damage).

  2. Вдобавок к ним у него должен быть новый атрибут: fire_breath_damage (урон огненным дыханием).

  3. Он должен уметь выполнять уникальную атаку огнём.

Если мы попробуем написать конструктор __init__ для Дракона по аналогии с Enemy, мы столкнёмся с дублированием кода:

# Плохой пример с дублированием кода
class Dragon(Enemy):
    def __init__(self, name, health, damage, fire_breath_damage):
        self.name = name                 # Этот код уже есть в Enemy
        self.health = health             # Этот код уже есть в Enemy
        self.damage = damage             # Этот код уже есть в Enemy
        self.fire_breath_damage = fire_breath_damage # А это новый атрибут

Это работает, но мы снова вернулись к копипасте. Есть способ лучше.

Волшебное слово super()

В Python есть встроенная функция super(), которая даёт нам доступ к методам родительского класса. Это похоже на то, как если бы дочерний класс говорил: «Я знаю, что я особенный, но сначала я хочу попросить родителя сделать свою часть работы».

Давайте перепишем конструктор Dragon с использованием super().

class Dragon(Enemy):
    def __init__(self, name, health, damage, fire_breath_damage):
        # 1. Вызываем __init__ родительского класса (Enemy)
        #    и передаём ему те параметры, за которые он отвечает.
        super().__init__(name, health, damage)

        # 2. А теперь добавляем новый, уникальный атрибут для Дракона.
        self.fire_breath_damage = fire_breath_damage

Что здесь произошло?

  1. super().__init__(name, health, damage) — эта строка находит родительский класс (Enemy) и вызывает его метод __init__. Родитель выполняет свою работу: присваивает self.name, self.health и self.damage.

  2. self.fire_breath_damage = fire_breath_damage — после того как родитель отработал, мы добавляем то, что характерно только для Дракона.

Никакого дублирования. Код чистый и логичный.

Добавляем новые и расширяем старые методы

Теперь, когда у нашего Дракона есть уникальный атрибут, давайте дадим ему новые способности.

  1. Добавим совершенно новый метод, которого нет в Enemy.

    # (внутри класса Dragon)
    def breathe_fire(self, target):
        print(f"'{self.name}' выдыхает пламя на {target.name}, нанося {self.fire_breath_damage} урона!")
        target.take_damage(self.fire_breath_damage)
    
  2. Расширим существующий метод attack. Мы хотим, чтобы обычная атака дракона была двойной: сначала он бьёт лапой (как любой Enemy), а потом дополнительно опаляет врага дыханием. И здесь нам снова поможет super()!

    # (внутри класса Dragon)
    def attack(self, target):
        # Сначала выполняем обычную атаку из родительского класса
        super().attack(target)
        
        # А потом добавляем новое действие
        print(f"Вдобавок '{self.name}' опаляет {target.name} своим дыханием!")
        target.take_damage(self.fire_breath_damage // 2) # Например, половину урона от спец. атаки
    

Здесь super().attack(target) выполняет код из метода attack класса Enemy (выводит сообщение о базовой атаке и наносит урон). Сразу после этого выполняется дополнительный код, который мы написали в Dragon.

Собираем всё вместе и смотрим в действии

Вот полный код нашего класса Dragon и пример его использования.

# (Предполагается, что классы Enemy и Hero уже определены выше)

class Dragon(Enemy):
    def __init__(self, name, health, damage, fire_breath_damage):
        # Вызываем конструктор родителя
        super().__init__(name, health, damage)
        # Добавляем свой атрибут
        self.fire_breath_damage = fire_breath_damage

    def attack(self, target):
        # Вызываем атаку родителя
        super().attack(target)
        # Добавляем своё действие
        bonus_damage = self.fire_breath_damage // 2
        print(f"Вдобавок '{self.name}' опаляет {target.name} своим дыханием, нанося ещё {bonus_damage} урона!")
        target.take_damage(bonus_damage)
        
    def breathe_fire(self, target):
        print(f"'{self.name}' выдыхает столб пламени на {target.name}, нанося {self.fire_breath_damage} урона!")
        target.take_damage(self.fire_breath_damage)


# --- Проверяем Дракона в бою ---

hero = Hero("Рыцарь", 250)
dragon = Dragon(name="Игнис", health=300, damage=20, fire_breath_damage=40)

# Дракон выполняет свою расширенную атаку
dragon.attack(hero)

# Дракон использует свою уникальную способность
dragon.breathe_fire(hero)

Запустив этот код, вы увидите примерно следующий вывод:

Враг 'Игнис' с 300 HP появился в мире!
'Игнис' атакует Рыцарь и наносит 20 урона!
У 'Рыцарь' осталось 230 здоровья.

Вдобавок 'Игнис' опаляет Рыцарь своим дыханием, нанося ещё 20 урона!
У 'Рыцарь' осталось 210 здоровья.

'Игнис' выдыхает столб пламени на Рыцарь, нанося 40 урона!
У 'Рыцарь' осталось 170 здоровья.

Как видите, super() — это невероятно мощный инструмент. Он позволяет нам строить классы как конструктор: брать готовые блоки от родителя и добавлять к ним новые детали, создавая более сложные и интересные объекты.

Теперь, когда мы освоили простое наследование и его расширение, пора взглянуть на то, как можно выстраивать целые иерархии классов. В следующем разделе мы создадим класс Undead, от которого, в свою очередь, будет наследоваться Skeleton.

5. Многоуровневое наследование — классы Undead и Skeleton

До сих пор мы рассматривали простую двухуровневую структуру: Родитель -> Потомок (Enemy -> Goblin). Но что, если нам нужна более глубокая и логичная классификация? Например, в нашей игре есть несколько видов нежити: скелеты, зомби, призраки. Все они — враги, но у них есть общая черта — они нежить.

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

Enemy -> Undead -> Skeleton

Давайте построим эту цепочку шаг за шагом.

Шаг 1: Создаём промежуточный класс Undead

Undead (Нежить) — это всё ещё враг, поэтому он должен наследоваться от Enemy. Но у него будут свои особенности, которые мы хотим передать всем его потомкам (скелетам, зомби и т.д.). Например, нежить не чувствует боли так, как живые, и может пугать своих противников.

# (Предполагается, что класс Enemy уже определён)

class Undead(Enemy):
    # Переопределим получение урона, чтобы добавить "атмосферы"
    def take_damage(self, amount):
        print(f"'{self.name}' не чувствует боли, но получает {amount} урона.")
        # Вызываем оригинальный метод take_damage из класса Enemy, чтобы не дублировать логику
        super().take_damage(amount)
        
    # Добавим уникальный метод для всей нежити
    def scare(self, target):
        print(f"'{self.name}' издает леденящий душу стон, пугая {target.name}!")

Обратите внимание, мы снова использовали super() в методе take_damage. Мы не захотели заново писать логику уменьшения здоровья и проверки на is_alive(), а просто добавили новое сообщение и передали управление родительскому методу.

Класс Undead — это полноценный класс. Мы могли бы создать его экземпляр (undead_spirit = Undead(...)), но его главная цель — служить «шаблоном» для более конкретных видов нежити.

Шаг 2: Создаём финальный класс Skeleton

Теперь создадим Скелета. Скелет — это нежить, поэтому он будет наследоваться от Undead, а не от Enemy. Благодаря этому он автоматически получит всё и от Undead (метод scare), и от Enemy (методы attack, __init__ и is_alive).

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

Нам понадобится модуль random для определения вероятности.

import random

# (Классы Enemy и Undead определены выше)

class Skeleton(Undead):
    def __init__(self, name, health, damage):
        # Используем super() для вызова конструктора родителя (в данном случае, Undead)
        super().__init__(name, health, damage)
        
    # Переопределяем метод take_damage ещё раз!
    def take_damage(self, amount):
        # С вероятностью 30% (0.3) скелет полностью избегает урона
        if random.random() < 0.3:
            print(f"Атака проходит сквозь кости '{self.name}', не нанося урона!")
        else:
            # Если увернуться не удалось, вызываем логику родителя (Undead)
            super().take_damage(amount)

Что мы сделали:

  1. Наследовали Skeleton от Undead.

  2. В методе take_damage добавили свою уникальную логику: с помощью random.random() (которая возвращает число от 0.0 до 1.0) мы даём скелету 30% шанс ничего не делать, то есть увернуться.

  3. Если же увернуться не удалось, мы не пишем код заново, а снова используем super().take_damage(amount). В этот раз super() обратится к родительскому классу Undead, который, в свою очередь, выведет своё сообщение и обратится к Enemy для непосредственного уменьшения здоровья.

Проверяем всю иерархию в действии

Давайте создадим скелета и посмотрим, к каким методам у него есть доступ.

# (Классы Enemy, Hero, Undead, Skeleton определены выше)

hero = Hero("Паладин", 300)
skeleton = Skeleton(name="Костяной воин", health=80, damage=12)

# 1. Метод из самого "дальнего" предка - Enemy
skeleton.attack(hero)

# 2. Метод из промежуточного родителя - Undead
skeleton.scare(hero)

# 3. Уникальный, переопределенный метод самого Skeleton
#    (результат может меняться из-за случайности)
print("\nПаладин атакует скелета...")
skeleton.take_damage(20)
skeleton.take_damage(20)
skeleton.take_damage(20)

Примерный вывод (он будет немного отличаться при каждом запуске):

Враг 'Костяной воин' с 80 HP появился в мире!
'Костяной воин' атакует Паладин и наносит 12 урона!
У 'Паладин' осталось 288 здоровья.

'Костяной воин' издает леденящий душу стон, пугая Паладин!

Паладин атакует скелета...
'Костяной воин' не чувствует боли, но получает 20 урона.
У 'Костяной воин' осталось 60 HP.
Атака проходит сквозь кости 'Костяной воин', не нанося урона!
'Костяной воин' не чувствует боли, но получает 20 урона.
У 'Костяной воин' осталось 40 HP.

Как видите, наш объект skeleton успешно использует методы со всех трёх уровней иерархии!

Многоуровневое наследование — это мощный инструмент для создания логичных, хорошо структурированных и легко расширяемых систем. Вместо хаотичного набора классов мы получаем аккуратное «генеалогическое древо».

В следующей, заключительной части, мы соберём всех наших врагов вместе и устроим небольшую симуляцию боя, чтобы увидеть, как они взаимодействуют.

6. Собираем всех вместе: симуляция боя

Мы создали целую иерархию врагов: простого Goblin, могучего Dragon и хитрого Skeleton. Каждый из них — наследник Enemy, но со своими уникальными чертами.

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

Подготовка к бою

Для начала, давайте немного улучшим наш класс Hero, чтобы он тоже мог атаковать.

import random

# (Все классы врагов: Enemy, Goblin, Dragon, Undead, Skeleton должны быть определены выше)

class Hero:
    def __init__(self, name="Герой", health=200, damage=25):
        self.name = name
        self.health = health
        self.damage = damage
        print(f"Навстречу приключениям выходит {self.name} с {self.health} HP!\n")

    def attack(self, target):
        print(f"'{self.name}' наносит удар по '{target.name}', причиняя {self.damage} урона.")
        target.take_damage(self.damage)

    def take_damage(self, amount):
        self.health -= amount
        if self.health <= 0:
            self.health = 0
            print(f"'{self.name}' пал в бою.")
        else:
            print(f"У '{self.name}' осталось {self.health} HP.")

    def is_alive(self):
        return self.health > 0

Теперь создадим по одному экземпляру каждого нашего врага и поместим их в один список.

# Создаем нашего героя
hero = Hero(name="Сэр Уильям", health=500, damage=30)

# Создаем армию врагов
enemies = [
    Goblin(name="Вонючка", health=50, damage=10),
    Dragon(name="Игнис Пожиратель", health=250, damage=25, fire_breath_damage=50),
    Skeleton(name="Безымянный Страж", health=100, damage=15)
]

Симуляция боя

А теперь — самое интересное. Мы напишем один-единственный цикл, который будет работать для любого врага из нашего списка, вне зависимости от его типа.

# --- Основной цикл боя ---
for enemy in enemies:
    print(f"\n--- {hero.name} встречает нового противника: {enemy.name}! ---")
    
    # Бой продолжается, пока оба участника живы
    while hero.is_alive() and enemy.is_alive():
        # Ход героя
        hero.attack(enemy)
        
        # Если враг еще жив после атаки героя, он отвечает
        if enemy.is_alive():
            enemy.attack(hero)
        print("-" * 20) # Разделитель для наглядности

    if not hero.is_alive():
        print("Игра окончена. Враги победили.")
        break # Завершаем цикл, если герой погиб
    else:
        print(f"{hero.name} одержал победу над {enemy.name}!")

# Финальное сообщение
if hero.is_alive():
    print(f"\nВеликая победа! {hero.name} выжил, одолев всех врагов!")

Запустите этот код. Вы увидите, как разворачивается сражение. И обратите внимание на ключевой момент:

Когда в коде вызывается enemy.attack(hero), Python сам понимает, какой именно объект скрывается за переменной enemy.

  • Если это Goblin, вызовется его переопределённый метод с "трусливым" сообщением.

  • Если это Dragon, вызовется его расширенный метод с двойной атакой.

  • Если это Skeleton, вызовется стандартный метод атаки, унаследованный от Enemy.

Точно так же, когда герой вызывает hero.attack(enemy), для скелета сработает его уникальная логика take_damage с шансом увернуться.

Это явление, когда один и тот же код (вызов метода attack) работает по-разному для разных объектов, называется полиморфизмом. Это третья и последняя из фундаментальных концепций ООП. Благодаря наследованию мы получили её практически бесплатно. Нам не пришлось писать громоздкие проверки вроде if тип_врага == "Дракон": .... Код получился чистым, гибким и легко читаемым.

В этом и заключается сила объектно-ориентированного подхода. Мы создали систему, которую очень легко расширять. Завтра мы захотим добавить Орка или Призрака — нам нужно будет просто создать новый класс, унаследовать его от Enemy или Undead и, возможно, переопределить пару методов. Основной боевой цикл при этом менять не придётся совсем.

В заключении мы подведём итоги нашего путешествия по миру наследования.

7. Заключение

А теперь ваша очередь закрепить знания! Давайте создадим похожую иерархию, но на примере животных. Цель — создать базовый класс Animal и несколько его потомков.

Шаг 1: Базовый класс Animal

Создайте класс Animal.

  • В конструкторе __init__ он должен принимать name и age.

  • У него должны быть два метода:

    • eat(): должен выводить сообщение вроде "{self.name} ест.".

    • make_sound(): должен выводить универсальное сообщение, например, "{self.name} издает звук.".

Шаг 2: Класс-наследник Dog

Создайте класс Dog, который наследуется от Animal.

  • Его конструктор __init__ должен принимать name, age и breed (порода). Не забудьте использовать super() для вызова родительского конструктора.

  • Переопределите метод make_sound(), чтобы он выводил "Гав!".

  • Добавьте новый, уникальный для собаки метод fetch(), который будет выводить "{self.name} принес(ла) палку.".

Шаг 3: Класс-наследник Cat

Создайте класс Cat, который также наследуется от Animal.

  • Его конструктор должен принимать name и age. Используйте super().

  • Переопределите метод make_sound(), чтобы он выводил "Мяу!".

  • Добавьте уникальный метод purr() (мурчать), который выводит "{self.name} мурлычет.".

Шаг 4: Многоуровневое наследование — Parrot
  1. Сначала создайте класс Bird, который наследуется от Animal. Добавьте ему уникальный метод fly(), который выводит "{self.name} летит.".

  2. Затем создайте класс Parrot (попугай), который наследуется от Bird.

  3. Переопределите в Parrot метод make_sound(), чтобы он выводил "Кар! Я хороший!".

Шаг 5: Собираем всех в зоопарке
  1. Создайте по одному объекту каждого из ваших финальных классов: Dog, Cat, Parrot.

  2. Поместите все эти объекты в один список под названием zoo_animals.

  3. Напишите цикл for animal in zoo_animals:, который для каждого животного в списке будет вызывать его метод make_sound().

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

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

Уверен, у вас все получится. Вперед, к практике!

Комментарии (4)


  1. StasTukalo
    29.10.2025 09:02

     мы говорим: «Сначала создадим общий шаблон "Враг" с базовыми характеристиками

    Не "мы" говорим, а хренвастенький архитектор говорит. А мы говорим- создадим класс "сущность" или "npc", а друг он или враг, дракон или библиотекарша- разберемся позднее))

    Шучу, если что. Хорошая статья.


  1. vladicuslist
    29.10.2025 09:02

    Для чего в классе Skeleton переопределен метод init без добавления новой функциональности?


    1. Leoleo435
      29.10.2025 09:02

      Сразу видно, что экземпляр класса Skeleton создаётся с полным набором свойств родительского класса и код остаётся открытым для изменений


  1. IisNuINu
    29.10.2025 09:02

    Статья хорошая. Но касаясь работы функции super у меня возникают смутные сомнения, в том что она даёт доступ к функциям РОДИТЕЛЬСКОГО класса! На самом деле в раннем питоне к функциям родительского класса так же обращались, но использовали при этом ИМЕНА родительского класса и это работало... кхм, работало почти всегда, а иногда нет ))). Почему же ввели super? и что она делает? Она предоставляет доступ к функциям первого ПРЕДШЕСТВУЮЩЕГО по ИЕРАРХИИ класса и это НЕ ВСЕГДА будет родительский класс!!! Да в случае одиночного наследования это всегда родительский класс. А вот в случае множественного наследования это не всегда так. Да в дальнейшей цепочке вызовов, скорее всего будет вызвана и функция родительского класса, но не следует забывать, что это именно будет цепочка вызовов, если функции классов правильно спроектированы и обеспечивают эту цепочку.