В Python существует методы для перегрузки операторов Python (<
, >
, =
и др.) и встроенных функций (len()
, str()
, repr()
и др.), использующих так называемые dunder или magic методы, и это возможность языка используется довольно часто. Например, перегрузка операторов используется для описания логики в случаях нестандартного использования операторов в пользовательских объектах (пример будет ниже). Но злоупотреблять перегрузкой также не стоит - не нужно делать перегрузку просто так, "чтобы было".
Дополнительно смотрите модуль стандартной библиотеки Python
operator
, экспортирует набор эффективных функций, соответствующих встроенным операторам Python. Например,operator.add(x, y)
эквивалентен выражениюx+y
. Многие имена функций используются для специальных методов без двойных подчеркиваний.
Например магический метод object.__str__()
перегружает функцию str()
, а так-же вызывается встроенными функциями format()
и print()
для вычисления неформального или красиво строкового представления объекта.
class Order: def __init__(self, price, num): self.price = price self.num = num def __getattr__(self, attrname): if attrname == "total": return self.price * self.num # неформальное строковое представление def __str__(self): return f'price={self.price}, num={self.num}, total={self.total}' >>> order = Order(10, 15) >>> str(order) # 'price=10, num=15, total=150'
Для иллюстрации того, зачем нужна перегрузка операторов и как она работает, рассмотрим реальный пример из жизни. В частности здесь пойдет речь о том, как переопределить поведение операторов +
и -
с помощью специальных методов __add__()
и __sub__()
классов Python.
Лучший способ понять, зачем перегружать операторы - это увидеть идею на практике.
Допустим, есть 24-часовой формат времени, а в разрабатываемой программе необходимо вычислять, например, какое время часы покажут через 10 часов. Если сейчас 18:00 вечера, то через 10 часов часы покажут, 4:00 утра, т.е. 18:00 часов + 10:00 часов = 4:00 часов. Итак, суммирование 24-часового времени не похоже на обычное суммирование натуральных, целых или действительных чисел.
Стоит цель - переопределить поведение операторов сложения и вычитания (+
, -
) так, чтобы правильно зафиксировать арифметику часов, чтобы можно было складывать и вычитать "время часов" (в часах) для получения соответствующих результатов не прибегая к модулю datetime
.
Изначально создадим класс Clock()
, в котором будем представлять время в формате "ЧЧ:ММ":
# файл test.py class Clock: def __init__(self, time: str): self.hour, self.min = [int(i) for i in time.split(':')] def __repr__(self) -> str: min = '0' + str(self.min) return str(self.hour) + ':' + min[-2:]
Обратите внимание: ожидается, что пользователь передаст аргумент time
в виде строки в формате "ЧЧ:ММ". Для определения консольного представления класса используется метод __repr__
, опять же в формате "ЧЧ:ММ".
Запустим файл test.py
в интерактивном режиме следующим образом python3 -i test.py
>>> t1 = Clock('10:30') >>> t1 # 10:30
Теперь создадим второй экземпляр класса Clock()
:
>>> t2 = Clock('19:45')
Если попытаться сложить эти экземпляры, то получим следующую ошибку:
>>> t1 + t2 # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # TypeError: unsupported operand type(s) for +: 'Clock' and 'Clock'
Проблема в том, что оператор сложения +
не понимает операнды класса Clock()
.
Можно исправить эту ошибку, добавив метод, связанный с сложением. В Python этот метод называется __add__
и требует двух аргументов. Первый, self
, всегда требуется, а второй, other
, представляет другой экземпляр класса. Например, a.__add__(b)
попросит объект a
добавить к себе объект b
. Это можно записать в стандартных обозначениях a + b
. В рассматриваемом случае сумму можно определить следующим образом:
# файл test.py class Clock: def __init__(self, time): self.hour, self.min = [int(i) for i in time.split(':')] def __add__(self, other): # метод сложения времени в формате "ЧЧ:ММ" hour, min = divmod(self.min + other.min, 60) hour = (hour + self.hour + other.hour) % 24 return self.__class__(str(hour) + ':' + str(min)) def __repr__(self) -> str: min = '0' + str(self.min) return str(self.hour) + ':' + min[-2:]
Обратите внимание, как здесь используется встроенная функция divmod()
. Она выполняет деление, но возвращает два значения - частное и остаток. Функция divmod()
преобразует общее количество минут в формат "ЧЧ:ММ". Число минут делится на 60 так, чтобы частное представляло часы, а остаток - минуты. Так как для обозначения часов используют цифры от 0 до 24, то здесь вычисляется общее количество часов по модулю 24.
Наконец, в конце выражение self.__class__(str(hour) + ':' + str(min))
используется для создания нового экземпляра класса Clock()
, чтобы результат можно было повторно использовать в последующих вычислениях.
Выйдем из консоли Python3 и снова запустим файл test.py
в интерактивном режиме следующим образом python3 -i test.py
>>> t1 = Clock('10:30') >>> t2 = Clock('19:45') >>> t1 + t2 # 6:15
Получен именно тот результат, который нужен. Аналогичным образом переопределяем поведение оператора -
, используя метод __sub__
:
# файл test.py class Clock(): def __init__(self, time): self.hour, self.min = [int(i) for i in time.split(':')] def __add__(self, other): # метод сложения времени в формате "ЧЧ:ММ" hour, min = divmod(self.min + other.min, 60) hour = (hour + self.hour + other.hour) % 24 return self.__class__(str(hour) + ':' + str(min)) def __sub__(self, other): # метод вычитания времени в формате "ЧЧ:ММ" hour, min = divmod(self.min - other.min, 60) hour = (hour + self.hour - other.hour) % 24 return self.__class__(str(hour) + ':' + str(min)) def __repr__(self) -> str: min = '0' + str(self.min) return str(self.hour) + ':' + min[-2:]
Теперь с объектами Clock()
можно работать напрямую, используя операторы +
и -
:
>>> t1 = Clock('10:30') >>> t2 = Clock('19:45') >>> t3 = Clock('16:16') >>> t1 - t2 + t3 # 6:15
Дополнительно смотрите "Часто перегружаемые арифметические операторы в Python".
Еще один "синтетический" пример ниже, с пояснениями в коде. Например есть класс Rope()
(веревка). У веревки есть только одно свойство - длина. К веревке можно привязать другую веревку, в следствии чего ее длина увеличится (длину входящую в узел учитывать не будем).
# создайте файл `test.py` class Rope: def __init__(self, length): self._length = length def __len__(self): # Переопределяем оператор `__len__` return self._length def __add__(self, other): # Переопределяем оператор сложения `+` if isinstance(other, self.__class__): # Если второй объект того же класса, то # возвращаем новый объект `Rope` с новой длиной return Rope(self._length + other._length) else: raise TypeError def __iadd__(self, other): # Переопределяем оператор # присваивания на месте `+=` if isinstance(other, self.__class__): # Если второй объект того же класса, то # изменяем длину существующего объекта self._length += other._length return self else: raise TypeError # запустим python3 в интерактивном режиме # $ python3 -i test.py # новая веревка длиной 10 >>> rope = Rope(10) # прибавляем объект веревки Rope(15), с длиной 15 >>> rope1 = rope + Rope(15) # длина исходной веревки не изменилась >>> len(rope) # 10 # длина объекта связанной веревки >>> len(rope1) # 25 >>> rope += Rope(5) >>> len(rope) # 15 # объект `Rope()` можно складывать # только с объектом `Rope()` >>> rope += 10 # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # File "test.py", line 28, in __iadd__ # raise TypeError # TypeError
__new__(cls[, ...])
- управляет созданием экземпляра. В качестве обязательного аргумента принимает класс (не путать с экземпляром). Должен возвращать экземпляр класса для его последующей передачи методу __init__
.__init__(self[, ...])
- конструктор класса.__del__(self)
- вызывается при удалении объекта сборщиком мусора.__repr__(self)
- вызывается встроенной функцией repr()
, использующиеся для внутреннего представления в Python.__str__(self)
- вызывается функциями str()
, print()
и format()
. Возвращает строковое представление объекта.__bytes__(self)
- вызывается функцией bytes()
при преобразовании к байтам.__format__(self, format_spec)
- используется функцией format
, а также методом строки str.format()
.__lt__(self, other)
- x < y
вызывает x.__lt__(y)
.__le__(self, other)
- x ≤ y
вызывает x.__le__(y)
.__eq__(self, other)
- x == y
вызывает x.__eq__(y)
.__ne__(self, other)
- x != y
вызывает x.__ne__(y)
.__gt__(self, other)
- x > y
вызывает x.__gt__(y)
.__ge__(self, other)
- x ≥ y
вызывает x.__ge__(y)
.__hash__(self)
- получение хэш-суммы объекта, например, для добавления в словарь.__bool__(self)
- вызывается при проверке истинности. Если этот метод не определён, то вызывается метод __len__
(объекты, имеющие ненулевую длину, считаются истинными).__call__(self[, args...])
- вызов экземпляра класса как функции.__len__(self)
- длина объекта.__getitem__(self, key)
- доступ по индексу или ключу.__setitem__(self, key, value)
- назначение элемента по индексу.__delitem__(self, key)
- удаление элемента по индексу.__reversed__(self)
- итератор из элементов, следующих в обратном порядке.__contains__(self, item)
- проверка на принадлежность элемента контейнеру.__concat__(self, other)
- a + b
для последовательностей.__int__(self)
- приведение к типу int
.__float__(self)
- приведение к типу float
.__round__(self[, n])
- округление.__abs__(self)
- по модулю, функция abs()
.__neg__(self)
- унарный -
.__pos__(self)
- унарный +
.__invert__(self)
- инверсия ~
.__add__(self, other)
- сложение x + y
вызывает x.__add__(y)
.__sub__(self, other)
- вычитание x - y
.__mul__(self, other)
- умножение x * y
.__truediv__(self, other)
- деление x / y
.__floordiv__(self, other)
- целочисленное деление x // y
.__mod__(self, other)
- остаток от деления x % y
.__divmod__(self, other)
- частное и остаток divmod(x, y)
.__pow__(self, other[, modulo])
- возведение в степень x ** y
и pow(x, y[, modulo])
.__matmul__(self, other)
- матричное умножение x @ y
.__lshift__(self, other)
- битовый сдвиг влево x << y
.__rshift__(self, other)
- битовый сдвиг вправо x >> y
.__and__(self, other)
- битовое И x & y
.__xor__(self, other)
- битовое ИСКЛЮЧАЮЩЕЕ ИЛИ x ^ y
.__or__(self, other)
- битовое ИЛИ x | y
.Далее идут методы, которые делают то же самое, что и арифметические операторы, но для аргументов, находящихся справа, и только в случае, если для левого операнда не определён соответствующий метод. Например, операция x + y
будет сначала пытаться вызвать x.__add__(y)
, и только в том случае, если это не получилось, будет пытаться вызвать y.__radd__(x)
:
__radd__(self, other)
,__rsub__(self, other)
,__rmul__(self, other)
,__rtruediv__(self, other)
,__rfloordiv__(self, other)
,__rmod__(self, other)
,__rdivmod__(self, other)
,__rpow__(self, other)
,__rlshift__(self, other)
,__rrshift__(self, other)
,__rand__(self, other)
,__rxor__(self, other)
,__ror__(self, other)
.__iadd__(self, other)
- эквивалентно x += y
.__isub__(self, other)
- эквивалентно x -= y
.__imul__(self, other)
- эквивалентно x *= y
.__itruediv__(self, other)
- эквивалентно x /= y
.__ifloordiv__(self, other)
- эквивалентно x //= y
.__imod__(self, other)
- эквивалентно x %= y
.__ipow__(self, other[, modulo])
- эквивалентно x **= y
.__ilshift__(self, other)
- эквивалентно x <<= y
.__irshift__(self, other)
- эквивалентно x >>= y
.__iand__(self, other)
- эквивалентно x &= y
.__ixor__(self, other)
- эквивалентно x ^= y
.__ior__(self, other)
- эквивалентно x |= y
.__imatmul__(self, other)
- эквивалентно x @= y
.