5.2. Волшебные методы, переопределение методов. Наследование
- Что такое наследование в ООП и зачем оно нужно
- Как работает множественное наследование и в чём его особенности в Python
- Что такое специальные (магические) методы и как они используются
- Как с помощью методов str, repr, add и других сделать поведение объектов более выразительным и удобным
- Ещё по теме
- Что дальше
В этой статье вы познакомитесь с принципами наследования в 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__и другие) позволяют описывать стандартное поведение объектов.- Перегрузка операторов делает взаимодействие с объектами интуитивно понятным и гибким.