Наследование в Python
Наследование в Python: подклассы, переопределение методов, super(), многоуровневое и множественное наследование, MRO и проверки isinstance — с примерами.
Понимание наследования в Python
Наследование позволяет создать новый класс на основе существующего. Новый класс (подкласс или дочерний класс) автоматически получает все атрибуты и методы существующего класса (суперкласса, базового класса или родительского класса). Затем вы можете добавить дополнительное поведение или выборочно переопределить унаследованное.
В этой главе рассматриваются:
- Синтаксис создания подклассов
- Как работает
super()и почему его стоит использовать - Переопределение методов и вызов родительской версии
- Одиночное, многоуровневое и множественное наследование
- Порядок разрешения методов (MRO) в Python
isinstance()иissubclass()для проверки типов во время выполнения- Распространённые ошибки
Перед тем как читать эту главу, убедитесь, что вы знакомы с классами и объектами Python. Для изучения других принципов ООП смотрите Инкапсуляция в Python и Абстрактные классы Python.
Синтаксис наследования
Чтобы создать подкласс, укажите имя родительского класса в скобках после имени нового класса:
class ParentClass:
pass
class ChildClass(ParentClass):
passМинимальный, но конкретный пример с базовым классом Vehicle и подклассом Car:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # delegate to Vehicle.__init__
self.num_doors = num_doors
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry started.
camry.stop() # Toyota Camry stopped.
print(camry.num_doors) # 4Car наследует start() и stop() от Vehicle, не дублируя их. Добавление num_doors — это единственная новая задача, которую нужно решить в Car.
Использование super()
super() возвращает объект-прокси, который делегирует вызовы методов родительскому классу. Чаще всего его используют внутри __init__, чтобы инициализировать атрибуты родителя перед добавлением собственных атрибутов дочернего класса:
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # runs Vehicle.__init__
self.num_doors = num_doorsПочему стоит предпочитать super() вместо прямого вызова Vehicle.__init__(self, ...)?
- Сопровождение: если вы переименуете или замените родительский класс, достаточно изменить одну строку.
- Множественное наследование:
super()следует порядку разрешения методов (MRO) Python, поэтому каждый класс в цепочке вызывается корректно. Жёсткое задание имени родителя обходит эту логику.
Вы можете вызывать super() для любого метода, не только для __init__:
class Car(Vehicle):
def start(self):
super().start() # call Vehicle.start first
print(f"({self.num_doors}-door model ready)")Переопределение методов
Подкласс переопределяет метод родителя, определяя метод с тем же именем. Python всегда вызывает наиболее производную версию первой:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def start(self):
print(f"{self.make} {self.model} revved the engine and started.")
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry revved the engine and started.
camry.stop() # Toyota Camry stopped. (inherited, not overridden)Если нужно расширить, а не заменить поведение родителя, вызовите super() внутри переопределяемого метода:
class Car(Vehicle):
def start(self):
super().start() # Vehicle.start runs first
print("Seatbelt reminder: buckle up!") # then add the extra stepНаследование атрибутов класса
Наследование применяется и к атрибутам класса (общим для всех экземпляров). Подкласс может переопределять их так же, как и методы:
class Vehicle:
wheels = 4
class Motorcycle(Vehicle):
wheels = 2 # override the class attribute
class Car(Vehicle):
pass # inherits wheels = 4
print(Motorcycle.wheels) # 2
print(Car.wheels) # 4
print(Vehicle.wheels) # 4Виды наследования
Одиночное наследование
Один дочерний класс, один родительский — наиболее распространённый вариант.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
dog = Dog("Rex")
print(dog.speak()) # Rex says woof!Многоуровневое наследование
Подкласс сам может быть унаследован, образуя цепочку:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year)
self.num_doors = num_doors
def start(self):
print(f"{self.make} {self.model} started.")
class ElectricCar(Car):
def __init__(self, make, model, year, range_km):
super().__init__(make, model, year) # calls Car.__init__
self.range_km = range_km
def start(self):
print(f"{self.make} {self.model} powered up silently.")
def charge(self):
print(f"Charging... range is {self.range_km} km.")
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
tesla.start() # Tesla Model 3 powered up silently.
tesla.charge() # Charging... range is 580 km.
tesla.stop() # Tesla Model 3 stopped. (inherited from Vehicle)ElectricCar находится на два уровня ниже Vehicle. Каждый вызов super().__init__ поднимается на один уровень вверх по цепочке, так что все три метода __init__ выполняются.
Множественное наследование
Python позволяет классу наследоваться от нескольких родителей. Перечислите их в скобках через запятую:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Swimmer:
def swim(self):
return f"{self.name} swims."
class Flyer:
def fly(self):
return f"{self.name} flies."
class Duck(Animal, Swimmer, Flyer):
def speak(self):
return f"{self.name} says quack!"
donald = Duck("Donald")
print(donald.speak()) # Donald says quack!
print(donald.swim()) # Donald swims.
print(donald.fly()) # Donald flies.Множественное наследование мощно, но усложняет код. Используйте примеси (mixins) — небольшие однозадачные классы, добавляющие одну возможность, — чтобы сохранить управляемость. Swimmer и Flyer выше — именно такой паттерн.
Порядок разрешения методов (MRO)
При поиске метода или атрибута Python следует детерминированному порядку поиска, называемому порядком разрешения методов (MRO). Для одиночного наследования порядок очевиден (дочерний → родительский → прародительский → object). Для множественного наследования Python использует алгоритм линеаризации C3, чтобы получить однозначный порядок.
Изучить MRO любого класса можно с помощью .__mro__ или help():
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Swimmer'>, <class 'Flyer'>, <class 'object'>)MRO важнее всего тогда, когда несколько родителей определяют один и тот же метод. Python выбирает первый класс в MRO, в котором этот метод определён. Именно поэтому super() важен в иерархиях с множественным наследованием: каждый класс в MRO вызывает super(), что даёт шанс выполниться каждому классу в цепочке.
class Base:
def greet(self):
print("Hello from Base")
class Left(Base):
def greet(self):
print("Hello from Left")
super().greet()
class Right(Base):
def greet(self):
print("Hello from Right")
super().greet()
class Child(Left, Right):
def greet(self):
print("Hello from Child")
super().greet()
Child().greet()
# Hello from Child
# Hello from Left
# Hello from Right
# Hello from BaseКаждый вызов super() следует MRO (Child → Left → Right → Base), поэтому каждый метод greet() выполняется ровно один раз, несмотря на то что Base является родителем и Left, и Right. Это проблема ромба — MRO Python решает её чисто.
Проверка наследования во время выполнения
isinstance(obj, cls)
Возвращает True, если obj является экземпляром cls или любого его подкласса:
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
print(isinstance(tesla, ElectricCar)) # True
print(isinstance(tesla, Car)) # True — Car is a parent
print(isinstance(tesla, Vehicle)) # True — Vehicle is a grandparent
print(isinstance(tesla, str)) # FalseЭто надёжнее, чем сравнение type(obj) == Car, которое возвращает False для подклассов.
issubclass(sub, cls)
Возвращает True, если sub является подклассом cls (включая сам класс):
print(issubclass(ElectricCar, Vehicle)) # True
print(issubclass(Car, ElectricCar)) # False
print(issubclass(Car, Car)) # True — a class is a subclass of itselfРаспространённые ошибки
Забыли вызвать super().__init__()
Если __init__ дочернего класса не вызывает super().__init__(), атрибуты родителя никогда не устанавливаются. Любой метод, который ожидает их наличия, вызовет AttributeError:
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
# forgot super().__init__(...)
self.num_doors = num_doors
c = Car("Toyota", "Camry", 2023)
c.start() # AttributeError: 'Car' object has no attribute 'make'Переопределение без вызова super(), когда нужно было расширить
Если вы переопределяете метод и забываете super(), родительская версия никогда не выполняется. Это незаметно убирает поведение, от которого может зависеть другой код.
Глубокие иерархии множественного наследования
Более двух уровней множественного наследования очень сложно поддерживать. Если вы обнаружили, что пишете class Foo(A, B, C, D), подумайте об использовании композиции — хранении экземпляров в виде атрибутов — вместо этого.
Преимущества наследования
- Повторное использование кода — общая логика находится в одном месте; подклассы получают её бесплатно.
- Расширяемость — вы можете добавить или изменить поведение в подклассе, не трогая родителя, что оставляет существующих пользователей без изменений.
- Полиморфизм — функции, принимающие
Vehicle, одинаково хорошо работают с любымCar,MotorcycleилиElectricCar. Смотрите Полиморфизм в Python для полного понимания.