W3docs

Наследование в 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)  # 4

Car наследует 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), подумайте об использовании композиции — хранении экземпляров в виде атрибутов — вместо этого.

Преимущества наследования

  1. Повторное использование кода — общая логика находится в одном месте; подклассы получают её бесплатно.
  2. Расширяемость — вы можете добавить или изменить поведение в подклассе, не трогая родителя, что оставляет существующих пользователей без изменений.
  3. Полиморфизм — функции, принимающие Vehicle, одинаково хорошо работают с любым Car, Motorcycle или ElectricCar. Смотрите Полиморфизм в Python для полного понимания.

Практика

Практика
Which of the following statements about Python inheritance are correct according to the information provided on the specified URL?
Which of the following statements about Python inheritance are correct according to the information provided on the specified URL?
Was this page helpful?