$ sudo teach IT
🐍 Урок 3.5 · Практика

Практика: Animal → Dog / Cat

В этом уроке мы не просто читаем теорию — мы строим живую иерархию классов шаг за шагом. К концу урока у вас будет полноценная система с базовым классом, дочерними классами, переопределёнными методами, полиморфизмом и трёхуровневым наследованием.

🎯 Что мы построим

Мы напишем небольшую программу про животных. Вот итоговая структура, которую получим:

🐾
Animal
Базовый класс. Хранит имя и возраст, умеет «говорить» и представлять себя строкой.
🐶
Dog(Animal)
Наследует Animal. Говорит «Гав». Умеет приносить палку — метод fetch().
🐱
Cat(Animal)
Наследует Animal. Говорит «Мяу». Умеет мурлыкать — метод purr().
🐕
Puppy(Dog)
Наследует Dog. Трёхуровневая цепочка: Puppy → Dog → Animal.
Зачем такая иерархия? Она отражает реальный мир: у всех животных есть общие черты (имя, возраст, звук), а у каждого вида — свои особенности. Именно для этого и нужно наследование.

🐾 Шаг 1: Проектируем базовый класс Animal

Начинаем с самого общего — с того, что есть у любого животного. Открывайте редактор и пишите вместе со мной.

class Animal:
    def __init__(self, name, age):
        self.name = name   # имя животного
        self.age = age     # возраст в годах

    def speak(self):
        return "..."       # базовый класс не знает, какой звук

    def __str__(self):
        return f"{self.name} (возраст: {self.age} лет)"

Разбираем по строчкам:

__init__(self, name, age)
Конструктор принимает два параметра и сохраняет их как атрибуты экземпляра через self.
speak()
Возвращает «...» — заглушка. Дочерние классы переопределят этот метод под свой звук.
__str__()
Магический метод — работает когда пишем print(animal). Возвращает читаемую строку.

Проверим, что класс работает:

a = Animal("Безымянный", 3)
print(a)          # Безымянный (возраст: 3 лет)
print(a.speak())  # ...
💡 Почему speak() возвращает "..."? Потому что Animal — абстрактное понятие. Реальный «животный звук» есть только у конкретного вида. Заглушка явно показывает: «этот метод предназначен для переопределения».

🐶 Шаг 2: Класс Dog — наследование и уникальные методы

Теперь создаём собаку. Собака — это животное, но с конкретным звуком и особым умением.

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # вызываем конструктор Animal
        self.breed = breed           # добавляем породу — только у Dog

    def speak(self):
        return f"{self.name} говорит: Гав! 🐶"

    def fetch(self, item="палку"):
        return f"{self.name} приносит {item}!"

Ключевой момент — super().__init__():

Когда мы пишем super().__init__(name, age), мы говорим Python: «сначала запусти конструктор родительского класса». Это важно — именно там создаются атрибуты self.name и self.age. Если не вызвать super(), Dog не будет знать своего имени и возраста!

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

  • Расширили конструктор — добавили параметр breed, которого нет в Animal
  • Переопределили speak() — теперь собака говорит «Гав», а не «...»
  • Добавили fetch() — уникальный метод только для Dog

Проверяем:

dog = Dog("Рекс", 3, "Немецкая овчарка")
print(dog)           # Рекс (возраст: 3 лет)  ← __str__ из Animal!
print(dog.speak())   # Рекс говорит: Гав! 🐶
print(dog.fetch())   # Рекс приносит палку!
print(dog.breed)     # Немецкая овчарка
Обратите внимание: print(dog) вызывает __str__ из Animal — мы не переопределяли его в Dog, поэтому Python сам нашёл его выше по цепочке наследования. Это и есть суть наследования: не повторять то, что уже написано.

🐱 Шаг 3: Класс Cat — та же логика, другой характер

Cat строится ровно по той же схеме, что и Dog. Это не случайно — единообразие делает код предсказуемым.

class Cat(Animal):
    def __init__(self, name, age, indoor=True):
        super().__init__(name, age)  # снова вызываем Animal.__init__
        self.indoor = indoor         # домашняя или уличная кошка

    def speak(self):
        return f"{self.name} говорит: Мяу! 🐱"

    def purr(self):
        return f"{self.name} мурлычет: Мррр... 😸"

Здесь мы использовали параметр со значением по умолчанию — indoor=True. Это значит, что при создании кошки можно не указывать этот параметр, и Python сам подставит True.

cat1 = Cat("Мурка", 2)           # indoor=True по умолчанию
cat2 = Cat("Дворовый", 5, False) # уличный кот

print(cat1.speak())  # Мурка говорит: Мяу! 🐱
print(cat1.purr())   # Мурка мурлычет: Мррр... 😸
print(cat1.indoor)   # True
print(cat2.indoor)   # False
💡 Обратите внимание на паттерн: Animal хранит общее, Dog и Cat — каждый хранит своё. Метод speak() одинаково называется в обоих классах, но делает разное. Это называется переопределение метода (method overriding).

🔄 Шаг 4: Полиморфизм в действии

Теперь — самое интересное. Создадим список с разными животными и вызовем у каждого speak(). Смотрите, что произойдёт:

animals = [
    Dog("Рекс", 3, "Овчарка"),
    Cat("Мурка", 2),
    Dog("Шарик", 5, "Дворняга"),
    Cat("Тигр", 4, False),
]

for animal in animals:
    print(animal)          # вызывает __str__ из Animal
    print(animal.speak())  # вызывает speak() КАЖДОГО конкретного класса
    print("---")

Результат:

Рекс (возраст: 3 лет)
Рекс говорит: Гав! 🐶
---
Мурка (возраст: 2 лет)
Мурка говорит: Мяу! 🐱
---
Шарик (возраст: 5 лет)
Шарик говорит: Гав! 🐶
---
Тигр (возраст: 4 лет)
Тигр говорит: Мяу! 🐱
---
Это и есть полиморфизм. Один и тот же код — animal.speak() — работает по-разному в зависимости от того, кем является animal в реальности. Dog вызывает свой speak(), Cat — свой. Нам не нужно писать if isinstance(animal, Dog): — Python сам разберётся.

Благодаря полиморфизму можно добавить хоть 10 новых видов животных — и цикл for animal in animals продолжит работать без изменений.

🐕 Шаг 5: Расширение — класс Puppy(Dog)

Добавим щенка. Щенок — это маленькая собака, поэтому логично наследовать его от Dog, а не от Animal напрямую. Получим трёхуровневую цепочку.

class Puppy(Dog):
    def __init__(self, name, breed):
        super().__init__(name, age=0, breed=breed)  # щенку 0 лет
        self.tricks = []  # список выученных трюков

    def speak(self):
        return f"{self.name} говорит: Тяф-тяф! 🐕 (ещё маленький)"

    def learn_trick(self, trick):
        self.tricks.append(trick)
        return f"{self.name} выучил трюк: {trick}!"

Как работает super() в трёхуровневой цепочке:

Когда создаём Puppy("Пушок", "Спаниель"), Python выполняет цепочку вызовов:

  1. Puppy.__init__ → вызывает super().__init__, то есть Dog.__init__
  2. Dog.__init__ → вызывает super().__init__, то есть Animal.__init__
  3. Animal.__init__ → устанавливает self.name и self.age
  4. Возвращаемся в Dog.__init__ → устанавливает self.breed
  5. Возвращаемся в Puppy.__init__ → устанавливает self.tricks = []

Проверяем MRO (Method Resolution Order) — порядок поиска методов:

print(Puppy.__mro__)
# (<class 'Puppy'>, <class 'Dog'>, <class 'Animal'>, <class 'object'>)

Python ищет метод слева направо по этому списку. Например, если вызвать puppy.fetch():

1. Puppy
fetch() нет ❌
2. Dog
fetch() есть ✅
3. Animal
не дошли
4. object
не дошли
puppy = Puppy("Пушок", "Спаниель")
print(puppy)                     # Пушок (возраст: 0 лет)  ← из Animal
print(puppy.speak())             # Пушок говорит: Тяф-тяф! 🐕 (ещё маленький)
print(puppy.fetch("мячик"))      # Пушок приносит мячик!  ← из Dog
print(puppy.learn_trick("сидеть"))  # Пушок выучил трюк: сидеть!
print(puppy.tricks)              # ['сидеть']
Puppy унаследовал всё: name, age, breed из Animal и Dog, метод fetch() из Dog, метод __str__ из Animal. И добавил своё: tricks и learn_trick(). Это и есть сила наследования — строить новое поверх существующего.

📋 Финальный код целиком

Вот всё, что мы написали, собранное в один файл:

# ── Базовый класс ──────────────────────────────────────────
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return "..."

    def __str__(self):
        return f"{self.name} (возраст: {self.age} лет)"


# ── Дочерние классы ────────────────────────────────────────
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def speak(self):
        return f"{self.name} говорит: Гав! 🐶"

    def fetch(self, item="палку"):
        return f"{self.name} приносит {item}!"


class Cat(Animal):
    def __init__(self, name, age, indoor=True):
        super().__init__(name, age)
        self.indoor = indoor

    def speak(self):
        return f"{self.name} говорит: Мяу! 🐱"

    def purr(self):
        return f"{self.name} мурлычет: Мррр... 😸"


# ── Трёхуровневое наследование ─────────────────────────────
class Puppy(Dog):
    def __init__(self, name, breed):
        super().__init__(name, age=0, breed=breed)
        self.tricks = []

    def speak(self):
        return f"{self.name} говорит: Тяф-тяф! 🐕 (ещё маленький)"

    def learn_trick(self, trick):
        self.tricks.append(trick)
        return f"{self.name} выучил трюк: {trick}!"


# ── Полиморфизм в действии ─────────────────────────────────
animals = [
    Dog("Рекс", 3, "Овчарка"),
    Cat("Мурка", 2),
    Puppy("Пушок", "Спаниель"),
    Cat("Тигр", 4, False),
]

print("=== Перекличка животных ===")
for animal in animals:
    print(animal.speak())

✅ Что мы закрепили в этом уроке

🏗 Базовый класс
Animal хранит общее для всех животных. Дочерние классы наследуют это бесплатно.
🔑 super().__init__()
Всегда вызываем в дочернем конструкторе — иначе атрибуты родителя не создадутся.
🔄 Переопределение
Dog и Cat переопределяют speak() — каждый говорит по-своему.
🌀 Полиморфизм
Один цикл — разное поведение. Python сам выбирает нужный speak() для каждого объекта.
🔗 MRO
Puppy → Dog → Animal → object. Python ищет методы по этому порядку, слева направо.
➕ Расширение
Дочерний класс добавляет свои атрибуты и методы поверх родительских.

✏️ Задание для самостоятельной работы

Добавьте в программу класс Bird(Animal). Вот требования:

  1. Bird наследует Animal
  2. Конструктор принимает name, age, can_fly (булевый, по умолчанию True)
  3. Метод speak() возвращает «[имя] говорит: Чирик! 🐦»
  4. Метод fly(): если can_fly=True — возвращает «[имя] взлетает!», иначе — «[имя] не умеет летать»
  5. Создайте несколько птиц и добавьте их в список animals
  6. Убедитесь, что цикл for animal in animals работает без изменений
Подсказка: Пингвин — это Bird с can_fly=False. Попробуйте создать его и проверить, что fly() работает правильно.