Сообщить об ошибке.

Понимание перегрузки операторов в Python

В 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.

Примеры перегрузки операторов в классах 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

Список методов, которые часто перегружаются в классах Python.

  • __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.