Вы уже научились создавать классы и объекты. Теперь представьте, что вам нужно описать несколько похожих сущностей: например, в программе для зоомагазина есть Cat, Dog, Parrot. У каждого есть имя, возраст, метод make_sound(). Писать один и тот же код трижды? Конечно нет. Наследование позволяет создать родительский класс с общими свойствами, а дочерние классы добавят свои особенности.
1. Что такое наследование?
Наследование — это механизм, при котором один класс (дочерний) получает все атрибуты и методы другого (родительского). Дочерний класс может:
- использовать родительские методы без изменений,
- переопределить их под свои нужды,
- добавить новые методы и атрибуты.
Синтаксис: class Child(Parent):
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def make_sound(self):
print("Животное издаёт звук")
class Dog(Animal):
def make_sound(self):
print("Гав-гав!")
Создаём объект:
buddy = Dog("Бадди", 3)
print(buddy.name) # Бадди (унаследовано)
buddy.make_sound() # Гав-гав! (переопределено)
2. Функция super() — вызываем родителя
Когда вы переопределяете метод, иногда нужно сначала выполнить родительскую логику. Например, конструктор __init__ у родителя уже задаёт name и age, а в дочернем добавляется порода. Используйте super():
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # вызываем родительский __init__
self.breed = breed
def make_sound(self):
super().make_sound() # сначала родительский метод
print("А точнее — Гав-гав!")
super() возвращает временный объект родительского класса, позволяя вызывать его методы.
3. Множественное наследование
Python позволяет классу наследоваться от нескольких родителей. Это мощно, но используйте осторожно — может запутать.
class Flyer:
def fly(self):
print("Летает")
class Swimmer:
def swim(self):
print("Плавает")
class Duck(Flyer, Swimmer):
pass
duck = Duck()
duck.fly() # Летает
duck.swim() # Плавает
Проблема: если у двух родителей есть методы с одинаковыми именами, Python использует порядок наследования (слева направо).
4. Полиморфизм — один интерфейс, разная реализация
Полиморфизм позволяет работать с разными объектами через единый интерфейс. Если у разных классов есть метод make_sound, можно вызывать его, не заботясь о конкретном типе объекта.
def animal_sound(animal):
animal.make_sound()
animals = [Dog("Рекс", 2), Cat("Мурка", 3), Parrot("Кеша", 1)]
for a in animals:
animal_sound(a)
Вывод:
Гав-гав!
Мяу!
Чирик!
Функция animal_sound не знает, кто именно пришёл — главное, что умеет make_sound.
5. Типичные ошибки новичков
❌ Забыли вызвать super().__init__()
Если в дочернем классе есть свой __init__ и вы не вызвали родительский, атрибуты родителя не инициализируются.
❌ Путаница между переопределением и перегрузкой
В Python нет перегрузки методов (нельзя написать два метода с одинаковым именем, но разными параметрами). Последний определённый метод заменяет предыдущий.
❌ Слишком глубокое наследование
Цепочка A -> B -> C -> D делает код сложным для понимания. Лучше композиция, чем наследование (правило: «предпочитайте композицию наследованию»).
❌ Множественное наследование с конфликтами
Если два родителя имеют метод с одинаковым именем, Python берёт того, кто левее. Это может дать неожиданное поведение.
6. Живой пример: система фигур для рисования
Создадим иерархию геометрических фигур с методом area() и perimeter():
import math
class Shape:
def area(self):
raise NotImplementedError("Дочерний класс должен реализовать area()")
def perimeter(self):
raise NotImplementedError("Дочерний класс должен реализовать perimeter()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
# Полиморфизм в действии
shapes = [Rectangle(3, 4), Circle(5), Square(4)]
for s in shapes:
print(f"{s.__class__.__name__}: площадь = {s.area():.2f}, периметр = {s.perimeter():.2f}")
Вывод:
Rectangle: площадь = 12.00, периметр = 14.00
Circle: площадь = 78.54, периметр = 31.42
Square: площадь = 16.00, периметр = 16.00
Обратите внимание: Square наследует от Rectangle, поэтому автоматически получил методы area и perimeter — достаточно вызвать super().__init__.
7. Когда наследование — это плохо?
Иногда проще использовать композицию: когда класс содержит внутри объект другого класса, а не наследует его.
class Engine:
def start(self):
print("Двигатель запущен")
class Car:
def __init__(self):
self.engine = Engine() # композиция
def start(self):
self.engine.start()
Правило: наследуйте, когда дочерний класс действительно является разновидностью родительского (Dog — это Animal). Если же связь «имеет» (Car имеет Engine) — используйте композицию.
Заключение
Наследование и полиморфизм превращают код в стройную систему. Вы научились:
- создавать иерархии классов,
- переопределять методы и вызывать родительские через super(),
- использовать полиморфизм для единообразной работы с разными объектами,
- различать ситуации, когда лучше использовать композицию.
Следующая статья будет посвящена магическим методам (dunder-методам) — например, как сделать так, чтобы ваши объекты можно было складывать +, сравнивать < и красиво выводить через print. А пока — попробуйте создать иерархию для вашей любимой предметной области (сотрудники, транспорт, книги). Эксперименты с наследованием дадут вам мощный инструмент.
Делитесь в комментариях, какие классы вы унаследовали и с какими трудностями столкнулись.
Статья подготовлена для канала «Код как искусство». Подписывайтесь, чтобы писать код, который растёт, а не распухает.