Перегрузка операторов

На этом занятии рассмотрим способы перегрузки операторов в классах. Что это такое? Давайте для примера создадим такой простой класс Clock (часы). В каждом экземпляре этого класса будем хранить время в виде секунд, отсчитывая от 00:00 часов ночи. Соответственно, число секунд не должно превышать значения:

24∙60∙60 = 86400 (число секунд в одном дне)

Поэтому, в конструкторе класса перед присвоением выполним операцию вычисления остатка от деления на это число:

self.__secs = secs % self.__DAY

Затем, реализуем метод getFormatTime(), который возвращает указанное время в формате:

01:32:02

Получаем такую реализацию класса:

class Clock:
    __DAY = 86400   # число секунд в дне
    def __init__(self, secs:int):
        if not isinstance(secs, int):
            raise ValueError("Секунды должны быть целым числом")
 
        self.__secs = secs % self.__DAY
 
    def getFormatTime(self):
        s = self.__secs % 60            # секунды
        m = (self.__secs // 60) % 60    # минуты
        h = (self.__secs // 3600) % 24  # часы
        return f"{Clock.__getForm(h)}:{Clock.__getForm(m)}:{Clock.__getForm(s)}"
 
    @staticmethod
    def __getForm(x):
        return str(x) if x > 9 else "0"+str(x)

И, далее, мы можем его вот так создавать и выводить время в консоль:

c1 = Clock(100)
print( c1.getFormatTime() )

Но теперь предположим, что мы хотели бы складывать времена вот так:

c2 = Clock(200)
c3 = c1 + c2

Если сейчас запустить программу, то увидим ожидаемую ошибку, что два таких класса складывать нельзя. И вот для таких случаев в Python предусмотрена возможность перегрузки операторов, чтобы они стали делать нужную нам операцию. В данном случае нужно перегрузить оператор сложения («+»). В действительности, когда он выполняется с классами, то в классе вызывается специальный метод

__add__(self, other)

вот его нам и нужно переопределить. Запишем в классе Clock следующие строки:

def __add__(self, other):
    if not isinstance(other, Clock):
        raise ArithmeticError("Правый операнд должен быть типом Clock")
 
    return Clock(self.__secs + other.getSeconds())

и добавим геттер getSeconds:

def getSeconds(self):
    return self.__secs

Все, теперь при выполнении оператора сложения будет формироваться новый экземпляр класса Clock с суммой секунд от правого и левого операндов. Запускаем программу и видим, что все работает как мы и хотели.

Мало того, мы можем вычислять и вот такие операции:

c1 = Clock(100)
c2 = Clock(200)
c3 = Clock(300)
c4 = c1 + c2 + c3
print( c4.getFormatTime() )

То есть, мы получили универсальный оператор сложения для часов.

Теперь посмотрим как можно перегрузить операторы типа

+=

для этого нужно переопределить метод

__iadd__(self, other)

Сделаем это следующим образом:

def __iadd__(self, other):
    if not isinstance(other, Clock):
        raise ArithmeticError("Правый операнд должен быть типом Clock")
 
    self.__secs += other.getSeconds()
    return self

Смотрите, мы здесь меняем значение секунд в самом экземпляре класса self и возвращаем этот же экземпляр, не создавая нового. В итоге операцию:

c1 += c2

можно представить так:

Или же все наши перегруженные операторы записать и так:

c1 = Clock(100)
c2 = Clock(200)
c3 = Clock(300)
c1 += c2+c3

Как это будет в деталях работать, я думаю, вы уже догадались. По аналогии могут быть перегружены и другие подобные операторы. Вот их краткий список:

Оператор

Метод оператора

Оператор

Метод оператора

x+y

__add__(self, other)

x += y

__iadd__(self, other)

x-y

__sub__(self, other)

x -= y

__isub__(self, other)

x*y

__mul__(self, other)

x *= y

__imul__(self, other)

x/y

__truediv__(self, other)

x /= y

__itruediv__(self, other)

x//y

__floordiv__(self, other)

x //= y

__ifloordiv__(self, other)

x % y

__mod__(self, other)

x %= y

__imod__(self, other)

По аналогии можно перегружать операторы сравнения. Например, мы хотим сравнить время для двух часов:

if c1 == c2:
    print("Времена равны")

Для этого в классе Clock нужно переопределить метод:

def __eq__(self, other):
    if self.__secs == other.getSeconds():
        return True
    return False

Тогда при одинаковом числе секунд:

c1 = Clock(100)
c2 = Clock(100)

оператор == возвратит True:

if c1 == c2:
    print("Времена равны")

Или же, метод eq можно записать в более краткой форме:

def __eq__(self, other):
    return self.__secs == other.getSeconds()

Это будет одно и то же (по результату).

Далее, сравнение на неравенство можно сделать так:

def __nq__(self, other):
    return not self.__eq__(other)

И ниже записать:

c3 = Clock(300)
if c1 != c3:
    print("Времена не равны")

Как видите, это все работает довольно просто и очевидно. Основные операторы сравнения, следующие:

Оператор

Метод оператора

x == y

__eq__(self, other)

x != y

__nq__(self, other)

x < y

__lt__(self, other)

x <= y

__le__(self, other)

x > y

__gt__(self, other)

x >= y

__ge__(self, other)

Еще один интересный вариант перегрузки представляют методы доступа к объекту по ключу:

__getitem__(self, item)
__setitem__(self, key, value)

Например, мы хотим получать значения часа, минут и секунд, обращаясь к экземпляру класса вот так:

print( c1["hour"], c1["min"], c1["sec"] )

Для этого переопределим метод __getitem__:

def __getitem__(self, item):
    if not isinstance(item, str):
        raise ValueError("Ключ должен быть строкой")
 
    if item == "hour":
        return (self.__secs // 3600) % 24
    elif item == "min":
        return (self.__secs // 60) % 60
    elif item == "sec":
        return self.__secs % 60
 
    return "Неверный ключ"

И теперь, при запуске программы, видим желаемый результат.

c1 = Clock(80000)

У нас здесь идет некоторое дублирование кода, что нехорошо, но я сейчас не буду на это отвлекаться, вы уже сами самостоятельно можете улучшить эту программу.

Далее, используя переопределение метода __setitem__, можно реализовать функционал определения времени через ключи:

c1["hour"] = 10

Сам метод выглядит так:

def __setitem__(self, key, value):
    if not isinstance(key, str):
        raise ValueError("Ключ должен быть строкой")
 
    if not isinstance(value, int):
        raise ValueError("Значение должно быть целым числом")
 
    s = self.__secs % 60  # секунды
    m = (self.__secs // 60) % 60  # минуты
    h = (self.__secs // 3600) % 24  # часы
 
    if key == "hour":
        self.__secs = s + 60 * m + value * 3600
    elif key == "min":
        self.__secs = s + 60 * value + h * 3600
    elif key == "sec":
        self.__secs = value + 60 * m + h * 3600

Вот мы с вами рассмотрели основные принципы перегрузки операторов класса. И, если у вас возникнет необходимость в реализации подобного функционала, то теперь знаете как это делается.

Задания для самоподготовки

1. Напишите класс Point3D для хранения координат в трехмерном пространстве (x, y, z). Реализуйте перегрузку операторов сложения, вычитания, умножения и деления для этого класса. Также сделайте возможность сравнения координат между собой и запись/считывание значений через ключи: “x”, “y”, “z”.

2. Напишите класс Matrix для работы с матрицами. Реализуйте перегрузку операторов сложения и вычитания для матриц равных размеров. Перегрузите оператор умножения для матриц, которые могут быть перемножены. Также сделайте возможность сравнения матриц между собой (на равенство и неравенство).

3. Напишите класс Complex для работы с комплексными числами. Реализуйте операторы сложения, вычитания и умножения. Также сделайте возможность присвоения действительных и мнимых значений через ключи «rel» и «img» и через свойства rel, img, реализованных с помощью дескрипторов.