Что вы узнаете
В этом параграфе вы познакомитесь с принципами наследования в 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__
и другие) позволяют описывать стандартное поведение объектов. - Перегрузка операторов делает взаимодействие с объектами интуитивно понятным и гибким.