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

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

Что вы узнаете

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

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

Ключевые вопросы параграфа

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

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

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

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

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

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

1class Pencil:
2
3    def __init__(self, color="серый"):
4        self.color = color
5
6    def draw_picture(self):
7        return f"Нарисован рисунок цветом '{self.color}'."
8
9class Pen(Pencil):
10
11    def sign_document(self):
12        if self.color not in ("синий", "чёрный", "фиолетовый"):
13            return f"Ручкой цвета '{self.color}' нельзя подписать документ."
14        return f"Подписан документ."
15
16blue_pen = Pen(color="синий")
17print(blue_pen.draw_picture())
18print(blue_pen.sign_document())
19red_pen = Pen(color="красный")
20print(red_pen.draw_picture())
21print(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 — наследуется сразу от двух предыдущих и умеет приветствовать как формально, так и неформально.

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

1class GreetingFormal:
2
3    def __init__(self):
4        self.formal_greeting = "Добрый день,"
5
6    def greet_formal(self, name):
7        return f"{self.formal_greeting} {name}!"
8
9class GreetingInformal:
10
11    def __init__(self):
12        self.informal_greeting = "Привет,"
13
14    def greet_informal(self, name):
15        return f"{self.informal_greeting} {name}!"
16
17class GreetingMix(GreetingFormal, GreetingInformal):
18
19    def __init__(self):
20        GreetingFormal.__init__(self)
21        GreetingInformal.__init__(self)
22
23mixed_greeting = GreetingMix()
24print(mixed_greeting.greet_formal("Пользователь"))
25print(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. Здесь уже используется одиночное наследование, но мы добавим его в этот блок как продолжение темы:

1class Car:
2
3    def __init__(self, color, consumption, tank_volume, mileage=0):
4        self.color = color
5        self.consumption = consumption
6        self.tank_volume = tank_volume
7        self.reserve = tank_volume
8        self.mileage = mileage
9        self.engine_on = False
10
11    def start_engine(self):
12        if not self.engine_on and self.reserve > 0:
13            self.engine_on = True
14            return "Двигатель запущен."
15        return "Двигатель уже был запущен."
16
17    def stop_engine(self):
18        if self.engine_on:
19            self.engine_on = False
20            return "Двигатель остановлен."
21        return "Двигатель уже был остановлен."
22
23    def drive(self, distance):
24        if not self.engine_on:
25            return "Двигатель не запущен."
26        if self.reserve / self.consumption * 100 < distance:
27            return "Малый запас топлива."
28        self.mileage += distance
29        self.reserve -= distance / 100 * self.consumption
30        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."
31
32    def refuel(self):
33        self.reserve = self.tank_volume
34
35    def get_mileage(self):
36        return self.mileage
37
38    def get_reserve(self):
39        return self.reserve
40
41    def get_consumption(self):
42        return self.consumption
43
44class ElectricCar(Car):
45
46    def __init__(self, color, consumption, bat_capacity, mileage=0):
47        super().__init__(color, consumption, bat_capacity, mileage)
48        self.bat_capacity = bat_capacity
49
50    def drive(self, distance):
51        if not self.engine_on:
52            return "Двигатель не запущен."
53        if self.reserve / self.consumption * 100 < distance:
54            return "Малый запас заряда."
55        self.mileage += distance
56        self.reserve -= distance / 100 * self.consumption
57        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."
58
59    def recharge(self):
60        self.reserve = self.bat_capacity
61
62
63electric_car = ElectricCar(color="white", consumption=15, bat_capacity=90)
64print(electric_car.start_engine())
65print(electric_car.drive(100))

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

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

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

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

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

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

1print(electric_car)
2
3# Вывод программы:
4# <__main__.ElectricCar object at 0x000002365DDD8A00>

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

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

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

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

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

1electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
2print(electric_car.start_engine())
3print(electric_car.drive(100))
4print(electric_car)
5
6# Вывод программы:
7# Двигатель запущен.
8# Проехали 100 км. Остаток заряда: 75.0 кВт*ч.
9# Электромобиль. Цвет: белый. Пробег: 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 выбирает нужный метод в зависимости от контекста арифметической операции:

1class A:
2
3    def __init__(self):
4        self.value = 10
5
6    def __add__(self, other):
7        return "Выполняется метод __add__."
8
9    def __radd__(self, other):
10        return "Выполняется метод __radd__."
11
12    def __iadd__(self, other):
13        self.value += other
14        return self
15
16    def __str__(self):
17        return f"value: {self.value}."
18        
19a = A()
20print(a + 1)
21print(1 + a)
22a += 1
23print(a)

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

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

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

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

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

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

1electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
2print(repr(electric_car))
3electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
4print([electric_car, electric_car_1])
5
6# Вывод программы
7
8# ElectricCar('белый', 15, 90, 0)
9
10# [ElectricCar('белый', 15, 90, 0), ElectricCar('чёрный', 17, 80, 0)]

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

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

1def __add__(self, other):
2    new_car = ElectricCar(self.color,
3                          self.consumption + other.consumption,
4                          self.bat_capacity + other.bat_capacity,
5                          self.mileage + other.mileage)
6    new_car.reserve = self.reserve + other.reserve
7    return new_car
8
9# Код для проверки
10
11electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
12electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
13electric_car.start_engine()
14electric_car_1.start_engine()
15electric_car.drive(300)
16electric_car_1.drive(100)
17new_electric_car = electric_car + electric_car_1
18print(new_electric_car)
19
20# Вывод программы
21
22# Электромобиль. Цвет: белый. Пробег: 400 км. Остаток заряда: 108.0 кВт*ч

Ещё по теме

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

✅ Вы разобрались, как работают наследование и специальные методы в Python?

👉 Оценить этот параграф

Что дальше

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

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

А пока вы не ушли дальше — закрепите материал на практике:

  • Отметьте, что урок прочитан, при помощи кнопки ниже.
  • Пройдите мини-квиз, чтобы проверить, насколько хорошо вы усвоили тему.
  • Перейдите к задачам этого параграфа и потренируйтесь.
  • Перед этим загляните в короткий гайд о том, как работает система проверки.

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

Ключевые выводы параграфа

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

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф5.1. Объектная модель Python. Классы, поля и методы

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

Следующий параграф5.3. Модель исключений Python. Try, except, else, finally. Модули

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