5.2. Волшебные методы, переопределение методов. Наследование

В этой статье вы познакомитесь с принципами наследования в Python и научитесь строить производные классы на основе уже существующих. Вы разберётесь, как расширять и переопределять методы базовых классов, а также узнаете, что такое множественное наследование и как Python обрабатывает конфликты между родительскими классами.

Кроме того, вы узнаете, что такое специальные (или магические) методы Python и как они позволяют объектам взаимодействовать со встроенными функциями и операциями — например, print(), +, == или in.

Ключевые вопросы статьи

  • Что такое наследование в ООП и зачем оно нужно?
  • Как работает множественное наследование и в чём его особенности в Python?
  • Что такое специальные (магические) методы и как они используются?
  • Как с помощью методов __str__, __repr__, __add__ и других сделать поведение объектов более выразительным и удобным?

Что такое наследование в ООП и зачем оно нужно

В предыдущей статье вы узнали, как создавать собственные классы и описывать поведение объектов. Однако при работе с несколькими похожими классами быстро возникает повторение кода. Например, если у разных классов схожие атрибуты или действия, приходится копировать одни и те же методы.

Чтобы избежать дублирования, в объектно-ориентированном программировании применяется наследование.

Наследование позволяет создавать новый класс на основе уже существующего. Такой новый класс называется производным, или классом-наследником, а исходный класс — базовым. Производный класс унаследует все атрибуты и методы базового класса, но при этом может добавлять свои или изменять существующие.

Рассмотрим пример. Создадим базовый класс Pencil (карандаш), который хранит цвет и умеет рисовать. Затем на его основе создадим производный класс Pen (ручка). Ручка, как и карандаш, может рисовать, но вдобавок — подписывать документы (если цвет соответствует официальным требованиям):

class Pencil:

    def __init__(self, color="серый"):
        self.color = color

    def draw_picture(self):
        return f"Нарисован рисунок цветом '{self.color}'."

class Pen(Pencil):

    def sign_document(self):
        if self.color not in ("синий", "чёрный", "фиолетовый"):
            return f"Ручкой цвета '{self.color}' нельзя подписать документ."
        return f"Подписан документ."

blue_pen = Pen(color="синий")
print(blue_pen.draw_picture())
print(blue_pen.sign_document())
red_pen = Pen(color="красный")
print(red_pen.draw_picture())
print(red_pen.sign_document())

Вывод программы:

Нарисован рисунок цветом 'синий'.
Подписан документ.
Нарисован рисунок цветом 'красный'.
Ручкой цвета 'красный' нельзя подписать документ.

В этом примере класс Pen унаследовал все свойства класса Pencil — инициализацию атрибута color и метод draw_picture. Мы не писали их заново — они просто перешли от базового класса к производному. Мы лишь добавили новый метод sign_document, специфичный для ручки.

Когда интерпретатор Python вызывает метод у объекта, он сначала ищет его в самом классе. Если метод не найден, поиск продолжается в базовом классе. Если нужно — ещё на уровень выше. Если ни в одном из родительских классов метод не обнаружен, будет вызвано исключение AttributeError.

Таким образом, наследование — мощный механизм, позволяющий переиспользовать код, сокращать дублирование и строить иерархии классов, в которых общее поведение определено один раз, а различия добавляются по мере необходимости.

Как работает множественное наследование и в чём его особенности в Python

В Python один класс может наследоваться сразу от нескольких базовых классов. Это называется множественным наследованием. В этом случае производный класс получает доступ ко всем атрибутам и методам всех родительских классов.

Рассмотрим пример с приветствием. Напишем три класса:

  • GreetingFormal — при инициализации создаёт атрибут formal_greeting со значением "Добрый день,"; метод greet_formal() возвращает приветствие по имени.
  • GreetingInformal — при инициализации создаёт атрибут informal_greeting со значением "Привет,"; метод greet_informal() также возвращает приветствие.
  • GreetingMix — наследуется сразу от двух предыдущих и умеет приветствовать как формально, так и неформально.

Чтобы задать множественное наследование, нужно перечислить базовые классы через запятую в определении производного:

class GreetingFormal:

    def __init__(self):
        self.formal_greeting = "Добрый день,"

    def greet_formal(self, name):
        return f"{self.formal_greeting} {name}!"

class GreetingInformal:

    def __init__(self):
        self.informal_greeting = "Привет,"

    def greet_informal(self, name):
        return f"{self.informal_greeting} {name}!"

class GreetingMix(GreetingFormal, GreetingInformal):

    def __init__(self):
        GreetingFormal.__init__(self)
        GreetingInformal.__init__(self)

mixed_greeting = GreetingMix()
print(mixed_greeting.greet_formal("Пользователь"))
print(mixed_greeting.greet_informal("Пользователь"))

Вывод программы:

Добрый день, Пользователь!
Привет, Пользователь!

Класс GreetingMix унаследовал методы обоих классов. Чтобы корректно проинициализировать все атрибуты, в его методе __init__ мы явно вызываем инициализацию каждого базового класса.

Почему нельзя использовать super()

Функция super() позволяет обращаться к методам родительского класса без прямого указания его имени. Это особенно удобно при переопределении методов, например __init__, поскольку позволяет избежать дублирования кода и упрощает сопровождение программ.

Однако при множественном наследовании использовать super() нужно осторожно. Python применяет порядок разрешения методов (MRO — Method Resolution Order), чтобы определить, в каком порядке искать методы у родительских классов. Если в такой иерархии вызвать super().__init__() только один раз, то будет выполнен __init__ только первого родителя по MRO. Остальные инициализаторы будут проигнорированы, если не вызвать их вручную.

В примере выше оба родительских класса имеют собственные __init__. Если бы мы использовали super() вместо явного вызова GreetingInformal.__init__(self), то informal_greeting не был бы создан и попытка вызвать greet_informal() привела бы к ошибке AttributeError.

Множественное наследование бывает удобно, когда:

  • вы хотите объединить поведение из нескольких независимых классов;
  • базовые классы реализуют разные аспекты (например, приветствие, логирование, сохранение данных);
  • необходимо повторно использовать код без создания глубокой иерархии.

Но при этом важно понимать, как работает super() и как устроен MRO, чтобы избежать неожиданных ошибок.
Теперь рассмотрим наследование с переопределением. Возьмём уже знакомый класс Car и создадим на его основе ElectricCar. Здесь уже используется одиночное наследование, но мы добавим его в этот блок как продолжение темы:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption

class ElectricCar(Car):

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        super().__init__(color, consumption, bat_capacity, mileage)
        self.bat_capacity = bat_capacity

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас заряда."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity


electric_car = ElectricCar(color="white", consumption=15, bat_capacity=90)
print(electric_car.start_engine())
print(electric_car.drive(100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток заряда: 75.0 кВт*ч.

Класс ElectricCar унаследовал поведение Car, но переопределил метод drive и добавил свой метод recharge. Благодаря этому описание класса стало более компактным и гибким.

Множественное наследование — мощный инструмент, но использовать его нужно аккуратно. В Python поиск методов и атрибутов осуществляется по порядку разрешения методов (MRO). Если в базовых классах есть совпадающие имена, приоритет получит тот класс, что указан первым. Поэтому при множественном наследовании важно чётко продумывать структуру и порядок инициализации.

Что такое специальные (магические) методы и как они используются

Давайте посмотрим, что произойдёт, если передать в функцию print() объект класса ElectricCar. Добавим в программу следующий код:

print(electric_car)

# Вывод программы:
# <__main__.ElectricCar object at 0x000002365DDD8A00>

Такой результат говорит лишь о том, что переменная electric_car — это объект класса ElectricCar, размещённый по определённому адресу в памяти. Но мы можем сделать вывод более информативным.

Когда функция print() пытается отобразить объект, она вызывает встроенную функцию str(), которая, в свою очередь, вызывает метод __str__() класса. Если определить этот метод вручную, мы можем задать, что именно будет выводиться.

Добавим метод __str__() в класс ElectricCar:

def __str__(self):
    return f"Электромобиль. " \
           f"Цвет: {self.color}. " \
           f"Пробег: {self.mileage} км. " \
           f"Остаток заряда: {self.reserve} кВт*ч."

Проверим, как теперь работает вывод:

electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
print(electric_car.start_engine())
print(electric_car.drive(100))
print(electric_car)

# Вывод программы:
# Двигатель запущен.
# Проехали 100 км. Остаток заряда: 75.0 кВт*ч.
# Электромобиль. Цвет: белый. Пробег: 100 км. Остаток заряда: 75.0 кВт*ч.

Магические методы (специальные методы)

Магические методы (часто называются также специальными методами) — это методы, имена которых окружены двойными подчёркиваниями, например: __init__, __str__, __add__. Такие методы вызываются автоматически при выполнении встроенных операций и функций и позволяют настроить поведение объектов под нужды вашей программы.

Этот механизм называется перегрузкой операторов (operator overloading). Вы можете определить, как именно ваш объект будет вести себя при сложении, сравнении, индексировании, отображении и других операциях.

Вот несколько примеров часто используемых магических методов:

  • __repr__ — вызывается функцией repr() и возвращает строку, представляющую объект в виде, пригодном для отладки и воссоздания. Также используется при отображении объектов в коллекциях.
  • __str__ — вызывается функцией str() и при печати объектов. Должен возвращать удобочитаемое строковое представление.
  • __add__ — позволяет задать поведение для оператора +.
  • __eq__— определяет, как объекты сравниваются на равенство (==).
  • __len__ — используется функцией len() для получения длины объекта.

Ниже — более подробный обзор магических методов, сгруппированных по назначению: для отображения, арифметики, сравнения, измерения длины и других операций.

Методы для операций сравнения
  • __lt__(self, other) — <
  • __le__(self, other) — <=
  • __eq__(self, other) — ==
  • __ne__(self, other) — !=
  • __gt__(self, other) — >
  • __ge__(self, other) — >=
Метод вызова объекта как функции
  • __call__(self, *args, **kwargs) — вызывается, когда объект вызывается как функция: obj(...)
Методы для работы с объектом как с коллекцией
  • __getitem__(self, key) — obj[key]
  • __setitem__(self, key, value) — obj[key] = value
  • __delitem__(self, key) — del obj[key]
  • __len__(self) — len(obj)
  • __contains__(self, item) — item in obj
Математические операции
  • __add__(self, other) — +
  • __sub__(self, other) — -
  • __mul__(self, other) — *
  • __matmul__(self, other) — @
  • __truediv__(self, other) — /
  • __floordiv__(self, other) — //
  • __mod__(self, other) — %
  • __divmod__(self, other) — divmod(self, other)
  • __pow__(self, other) — **
  • __lshift__(self, other) — <<
  • __rshift__(self, other) — >>
  • __and__(self, other) — &
  • __xor__(self, other) — ^
  • __or__(self, other) — |
Обратные операции (если объект стоит справа)
  • __radd__,__rsub__, __rmul__, __rmatmul__, __rtruediv__,
  • __rfloordiv__, __rmod__, __rdivmod__, __rpow__, __rlshift__,
  • __rrshift__, __rand__, __rxor__, __ror__
Условно-изменяющие операции (in-place)
  • __iadd__, __isub__, __imul__, __imatmul__, __itruediv__,
  • __ifloordiv__, __imod__, __ipow__, __ilshift__, __irshift__,
  • __iand__, __ixor__, __ior__

Как с помощью методов str, repr, add и других сделать поведение объектов более выразительным и удобным

Магические методы делают классы гибкими и выразительными. Они позволяют создавать объекты, которые ведут себя как встроенные типы данных: можно реализовать индексацию, арифметические операции, перебор, преобразование в строку и многое другое. Всё это достигается за счёт специальных методов, вызываемых автоматически при использовании операторов и встроенных функций.

Посмотрим на конкретный пример — как работают методы __add__, __radd__ и __iadd__, которые отвечают за разные варианты сложения объектов.

Пример: разница между add, radd и iadd

Покажем на небольшом классе, как Python выбирает нужный метод в зависимости от контекста арифметической операции:

class A:

    def __init__(self):
        self.value = 10

    def __add__(self, other):
        return "Выполняется метод __add__."

    def __radd__(self, other):
        return "Выполняется метод __radd__."

    def __iadd__(self, other):
        self.value += other
        return self

    def __str__(self):
        return f"value: {self.value}."
        
a = A()
print(a + 1)
print(1 + a)
a += 1
print(a)

Вывод программы:

Выполняется метод __add__.
Выполняется метод __radd__.
value: 11.
  • a + 1 вызывает метод __add__;
  • 1 + a — __radd__, так как int не умеет складываться с A;
  • a += 1 — __iadd__, при этом важно вернуть сам объект (self), иначе в переменной a окажется None.

Метод repr: представление объекта как строки кода

Для удобной отладки объектов и их отображения в списках используют метод __repr__. Он возвращает строку, которая показывает, как можно было бы воссоздать объект:

def __repr__(self):
    return f"ElectricCar('{self.color}', " \
           f"{self.consumption}, " \
           f"{self.bat_capacity}, " \
           f"{self.mileage})"

Пример использования:

electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
print(repr(electric_car))
electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
print([electric_car, electric_car_1])

# Вывод программы

# ElectricCar('белый', 15, 90, 0)

# [ElectricCar('белый', 15, 90, 0), ElectricCar('чёрный', 17, 80, 0)]

Метод add: операция сложения для пользовательских объектов

С помощью метода __add__ можно задать поведение объекта при использовании оператора +. Например, сложим два электромобиля в один, суммируя их параметры:

def __add__(self, other):
    new_car = ElectricCar(self.color,
                          self.consumption + other.consumption,
                          self.bat_capacity + other.bat_capacity,
                          self.mileage + other.mileage)
    new_car.reserve = self.reserve + other.reserve
    return new_car

# Код для проверки

electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
electric_car.start_engine()
electric_car_1.start_engine()
electric_car.drive(300)
electric_car_1.drive(100)
new_electric_car = electric_car + electric_car_1
print(new_electric_car)

# Вывод программы

# Электромобиль. Цвет: белый. Пробег: 400 км. Остаток заряда: 108.0 кВт*ч

Ещё по теме

Полный список специальных методов с описанием можно посмотреть в документации.

Что дальше

В этой статье вы узнали, как создавать производные классы, расширять и переопределять их методы, а также применять множественное наследование. Вы научились описывать поведение объектов через специальные методы — такие, как __str__, __repr__, __add__ — и сделали свои классы более гибкими, выразительными и удобными в использовании.

В следующей статье мы разберём, как обрабатывать ошибки в Python с помощью конструкции try...except, как использовать блоки else и finally и как правильно структурировать код с использованием модулей.

Ключевые выводы статьи

  • Наследование позволяет создавать новые классы на основе существующих и повторно использовать код.
  • Методы можно расширять или полностью переопределять в производных классах.
  • Множественное наследование даёт доступ к функциональности нескольких базовых классов, но требует аккуратности в инициализации.
  • Специальные методы (__init__, __str__, __repr__, __add__ и другие) позволяют описывать стандартное поведение объектов.
  • Перегрузка операторов делает взаимодействие с объектами интуитивно понятным и гибким.