Наследование и super()
Представьте, что у вас уже есть чертёж «животное»: кличка, звук и умение говорить. Теперь нужен чертёж кошки. Перерисовывать всё заново незачем. Можно взять копию старого чертежа и приписать сбоку: «всё как у животного, но говорит иначе».
Без такой возможности код пришлось бы копировать. У кошки и собаки, как у любого животного, есть кличка и звук, и говорят они почти одинаково. Если копировать __init__ и методы в каждый класс, одну и ту же правку потом придётся вносить в нескольких местах. Класс может взять всё у другого класса и изменить только то, что отличается:
class Animal:
def __init__(self, name):
self.name, self.sound = name, "..."
def speak(self):
return f"{self.name} says {self.sound}"
class Cat(Animal):
def speak(self):
return f"{self.name} purrs"
print(Animal("Ox").speak()) # Ox says ...
print(Cat("Tom").speak()) # Tom purrsНаследование
Запись class Cat(Animal) означает наследование: Cat — дочерний класс, Animal — родительский. Дочерний класс получает все методы и атрибуты класса родителя.
Давайте разберём, что происходит при вызове Cat("Tom"):
- Своего
__init__уCatнет, поэтому Python берёт__init__классаAnimal. - Этот
__init__создаёт у кошки атрибутыnameиsound. Cat("Tom").speak()вызываетspeakклассаCatи возвращает"Tom purrs".
Переопределение метода
Python ищет метод сначала в классе экземпляра, потом в родительском. Метод speak класса Cat переопределяет метод родителя: для кошки находится он.
Поэтому один и тот же вызов работает с объектами разных классов, и каждый объект выполняет свой метод:
pets = [Animal("Ox"), Cat("Tom")]
print([pet.speak() for pet in pets]) # ['Ox says ...', 'Tom purrs']
print(isinstance(pets[1], Animal), isinstance(pets[0], Cat)) # True FalseЭкземпляр дочернего класса считается и экземпляром родительского: isinstance(pets[1], Animal) даёт True. Обратное неверно: Animal("Ox") — не Cat.
Код, который вызывает pet.speak(), класс питомца не проверяет: новому классу животных хватит своего метода speak. На собеседовании это называют полиморфизмом: один вызов, а поведение зависит от класса объекта.
Вызов super()
Если дочернему классу нужен свой __init__, он заменяет __init__ родителя целиком: Python сам вызывает только __init__ дочернего класса. Чтобы родительский код тоже выполнился, его вызывают через super():
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
self.sound = "woof"
rex = Dog("Rex", "husky")
print(rex.speak(), rex.breed) # Rex says woof huskyРазберём __init__ класса Dog по строкам:
super().__init__(name)запускает__init__классаAnimalдля того жеself: уrexпоявляютсяnameиsoundсо значением"...".self.breed = breedдобавляет породу.self.sound = "woof"заменяет звук.
Побеждает последнее присваивание: если задать sound до вызова super(), родительский __init__ снова запишет в него "...". Через super() вызывают и другие методы родителя: super().speak() внутри метода Dog вернёт текст метода Animal.
Здесь аналогия с копией чертежа заканчивается. Дочерний класс не копирует методы родителя к себе. Он хранит ссылку на родителя, и Python каждый раз ищет метод по цепочке: сначала в Dog, потом в Animal.
Частая ошибка
В дочернем __init__ забывают вызвать super().__init__():
class Bird(Animal):
def __init__(self, name):
self.sound = "tweet" # ✗
print(Bird("Kiwi").speak())AttributeError: 'Bird' object has no attribute 'name'Свой __init__ заменил родительский, и строка, которая задаёт self.name, не выполнилась. Если у родителя есть свой __init__, первой строкой дочернего __init__ пишите super().__init__(name).
Главное
class Cat(Animal)— наследование: дочерний класс получает методы и атрибуты родителя.- Метод с тем же именем в дочернем классе переопределяет метод родителя.
- Свой
__init__заменяет родительский целиком, поэтому его код вызывают черезsuper().__init__(...). - Экземпляр дочернего класса — это и экземпляр родительского:
isinstanceдаётTrue.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.