Практика: Animal → Dog / Cat
В этом уроке мы не просто читаем теорию — мы строим живую иерархию классов шаг за шагом. К концу урока у вас будет полноценная система с базовым классом, дочерними классами, переопределёнными методами, полиморфизмом и трёхуровневым наследованием.
🎯 Что мы построим
Мы напишем небольшую программу про животных. Вот итоговая структура, которую получим:
🐾 Шаг 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()) # ...
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
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 выполняет цепочку вызовов:
Puppy.__init__→ вызываетsuper().__init__, то естьDog.__init__Dog.__init__→ вызываетsuper().__init__, то естьAnimal.__init__Animal.__init__→ устанавливаетself.nameиself.age- Возвращаемся в
Dog.__init__→ устанавливаетself.breed - Возвращаемся в
Puppy.__init__→ устанавливаетself.tricks = []
Проверяем MRO (Method Resolution Order) — порядок поиска методов:
print(Puppy.__mro__)
# (<class 'Puppy'>, <class 'Dog'>, <class 'Animal'>, <class 'object'>)
Python ищет метод слева направо по этому списку. Например, если вызвать puppy.fetch():
puppy = Puppy("Пушок", "Спаниель")
print(puppy) # Пушок (возраст: 0 лет) ← из Animal
print(puppy.speak()) # Пушок говорит: Тяф-тяф! 🐕 (ещё маленький)
print(puppy.fetch("мячик")) # Пушок приносит мячик! ← из Dog
print(puppy.learn_trick("сидеть")) # Пушок выучил трюк: сидеть!
print(puppy.tricks) # ['сидеть']
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())
✅ Что мы закрепили в этом уроке
✏️ Задание для самостоятельной работы
Добавьте в программу класс Bird(Animal). Вот требования:
- Bird наследует Animal
- Конструктор принимает
name,age,can_fly(булевый, по умолчаниюTrue) - Метод
speak()возвращает «[имя] говорит: Чирик! 🐦» - Метод
fly(): еслиcan_fly=True— возвращает «[имя] взлетает!», иначе — «[имя] не умеет летать» - Создайте несколько птиц и добавьте их в список
animals - Убедитесь, что цикл
for animal in animalsработает без изменений
can_fly=False. Попробуйте создать его и проверить, что fly() работает правильно.