В Python "перегрузка операторов" (operator overloading) — это возможность изменять поведение стандартных операторов (таких как +, -, *, /, ==, <, [] и т. д.) для пользовательских классов. Это позволяет вашим объектам взаимодействовать с операторами интуитивно понятным способом, делая код более читаемым и выразительным.
Когда вы определяете специальные методы (их еще называют "магическими" методами или "dunder" методами, от "double underscore" — двойное подчеркивание, например, __add__), Python автоматически вызывает эти методы, когда вы используете соответствующий оператор с экземплярами вашего класса.
Как это работает?
Каждый оператор в Python соответствует определенному магическому методу:
Операция
Метод
Описание
+
__add__(self, other)
Сложение
—
__sub__(self, other)
Вычитание
*
__mul__(self, other)
Умножение
/
__truediv__(self, other)
Истинное деление (всегда возвращает float)
//
__floordiv__(self, other)
Целочисленное деление
%
__mod__(self, other)
Остаток от деления (модуль)
**
__pow__(self, other[, modulo])
Возведение в степень
<<
__lshift__(self, other)
Побитовый сдвиг влево
>>
__rshift__(self, other)
Побитовый сдвиг вправо
&
__and__(self, other)
Побитовое И
`
`
__or__(self, other)
^
__xor__(self, other)
Побитовое исключающее ИЛИ (XOR)
~
__invert__(self)
Побитовое НЕ (унарный оператор)
==
__eq__(self, other)
Равенство
!=
__ne__(self, other)
Неравенство
<
__lt__(self, other)
Меньше
<=
__le__(self, other)
Меньше или равно
>
__gt__(self, other)
Больше
>=
__ge__(self, other)
Больше или равно
[index] (получение)
__getitem__(self, key)
Доступ по индексу/ключу (например, obj[key])
[index] = value (установка)
__setitem__(self, key, value)
Установка значения по индексу/ключу (например, obj[key] = value)
Del obj[index] (удаление)
__delitem__(self, key)
Удаление элемента по индексу/ключу (например, del obj[key])
Len(obj)
__len__(self)
Длина объекта
Bool(obj)
__bool__(self)
Логическое преобразование (если не определен, используется __len__)
Str(obj)
__str__(self)
Строковое представление для пользователя (читабельное)
Repr(obj)
__repr__(self)
Строковое представление для разработчика (однозначное)
Obj() (вызов объекта)
__call__(self[, args…])
Вызов экземпляра как функции
In
__contains__(self, item)
Проверка на вхождение (item in obj)
+obj (унарный плюс)
__pos__(self)
Унарный плюс
-obj (унарный минус)
__neg__(self)
Унарный минус
Пример: Класс Vector
Давайте создадим класс Vector для двухмерных векторов и перегрузим операторы сложения, умножения и сравнения.
Python
Class Vector:
def __init__(self, x, y):
self. x = x
self. y = y
def __repr__(self):
"""
Представление объекта для разработчика (для отладки)
"""
return f"Vector({self. x}, {self. y})"
def __str__(self):
"""
Представление объекта для пользователя (для печати)
"""
return f"({self. x}, {self. y})"
# Перегрузка оператора сложения (+)
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self. x + other. x, self. y + other. y)
else:
raise TypeError("Можно складывать только Vector с Vector")
# Перегрузка оператора вычитания (-)
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self. x — other. x, self. y — other. y)
else:
raise TypeError("Можно вычитать только Vector из Vector")
# Перегрузка оператора умножения (*)
# Это может быть скалярное умножение (Vector * number)
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self. x * scalar, self. y * scalar)
else:
raise TypeError("Vector можно умножать только на числовой скаляр")
# Перегрузка оператора обратного умножения (*)
# Позволяет выполнять number * Vector
def __rmul__(self, scalar):
return self.__mul__(scalar) # Делегируем обычному __mul__
# Перегрузка оператора равенства (==)
def __eq__(self, other):
if isinstance(other, Vector):
return self. x == other. x and self. y == other. y
return NotImplemented # Указывает, что сравнение с другим типом не поддерживается
# Перегрузка оператора неравенства (!=)
def __ne__(self, other):
return not self.__eq__(other) # Часто можно реализовать через __eq__
# Перегрузка унарного минуса (-obj)
def __neg__(self):
return Vector(-self. x, — self. y)
# Создание экземпляров
V1 = Vector(2, 3)
V2 = Vector(5, 7)
V3 = Vector(2, 3) # Для проверки равенства
Print(f"v1: {v1}") # Вызовет __str__
Print(f"v2: {v2}") # Вызовет __str__
# Использование перегруженных операторов
V_sum = v1 + v2
Print(f"v1 + v2 = {v_sum}") # Вывод: v1 + v2 = (7, 10)
V_diff = v2 — v1
Print(f"v2 — v1 = {v_diff}") # Вывод: v2 — v1 = (3, 4)
V_scaled = v1 * 3
Print(f"v1 * 3 = {v_scaled}") # Вывод: v1 * 3 = (6, 9)
V_scaled_reverse = 4 * v2
Print(f"4 * v2 = {v_scaled_reverse}") # Вывод: 4 * v2 = (20, 28)
V_neg = — v1
Print(f"-v1 = {v_neg}") # Вывод: — v1 = (-2, -3)
Print(f"v1 == v2: {v1 == v2}") # Вывод: v1 == v2: False
Print(f"v1 == v3: {v1 == v3}") # Вывод: v1 == v3: True
Print(f"v1 != v2: {v1 != v2}") # Вывод: v1 != v2: True
# Попытка некорректной операции
Try:
v1 + 10
Except TypeError as e:
print(f"Ошибка: {e}") # Вывод: Ошибка: Можно складывать только Vector с Vector
Важные моменты и лучшие практики:
Читаемость и интуитивность: Перегружайте операторы только тогда, когда это делает код более читаемым и когда поведение оператора интуитивно понятно для вашего типа данных. Не злоупотребляйте этим. Например, умножение вектора на вектор (v1 * v2) может означать скалярное произведение, векторное произведение или поэлементное умножение — это может сбить с толку, если не договориться о конкретном значении. Возвращаемое значение: Большинство магических методов, соответствующих бинарным операторам, должны возвращать новый объект, а не изменять текущий (если это не оператор присваивания, такой как __iadd__). Это соответствует принципу неизменяемости для многих встроенных типов (например, сложение строк возвращает новую строку, а не изменяет существующую). __repr__ vs __str__:
__repr__ (представление для разработчика): Должен быть однозначным, по возможности, позволяющим воссоздать объект (Vector(2, 3)). Используется repr(). __str__ (представление для пользователя): Должен быть читаемым и удобным для человека ((2, 3)). Используется str() и print().
Обратные (reflected) операторы (__radd__, __rmul__ и т. д.): Если вы пытаетесь выполнить other + self (например, 3 + vector_obj), и other не знает, как сложить себя с вашим объектом, Python попробует вызвать __radd__ (reverse add) вашего объекта. Часто __radd__ и другие обратные операторы просто вызывают их прямые аналоги, если операция симметрична (как в примере с __rmul__). Операторы на месте (__iadd__, __isub__ и т. д.): Соответствуют операторам присваивания, таким как +=, -=. Они должны изменять объект на месте и возвращать self.
Python
# Пример __iadd__
Class MutableVector:
def __init__(self, x, y):
self. x = x
self. y = y
def __repr__(self):
return f"MutableVector({self. x}, {self. y})"
def __iadd__(self, other):
if isinstance(other, MutableVector):
self. x += other. x
self. y += other. y
return self # Важно вернуть self для операторов на месте
else:
return NotImplemented # Или вызвать TypeError
Mv1 = MutableVector(1, 1)
Mv2 = MutableVector(2, 2)
Print(f"Перед iadd: {mv1}") # Вывод: Перед iadd: MutableVector(1, 1)
Mv1 += mv2
Print(f"После iadd: {mv1}") # Вывод: После iadd: MutableVector(3, 3)
NotImplemented: При реализации операторов сравнения или бинарных операторов, если вы не можете (или не хотите) обрабатывать операцию с данным типом other, верните NotImplemented. Это позволяет Python попробовать другие варианты (например, __radd__ у other). Не вызывайте TypeError сразу, если это не гарантированно некорректная операция. Хеширование (__hash__): Если вы переопределяете __eq__, и ваш объект должен быть хешируемым (т. е. использоваться в качестве ключей словаря или элементов множества), вы также должны переопределить __hash__. Если объект изменяем, обычно не стоит его хешировать. Если вы определяете __eq__ и не определяете __hash__ (или устанавливаете __hash__ = None), объект по умолчанию становится нехешируемым.
Перегрузка операторов — это мощная функция, которая при правильном использовании может сделать ваш код более элегантным, интуитивно понятным и Pythonic.