Встроенные типы данных поддерживают множество стандартных операций. Например, числа поддерживают все арифметические операции, а строки мы можем складывать и умножать на число.

Однако, когда вы создаёте собственный класс, Python использует поведение по умолчанию и не знает, какое логическое значение вы вкладываете в операции сложения или сравнения для объектов этого класса.

Например, давайте вспомним, что в математике есть такое понятие как вектор – направленный отрезок, соединяющий две точки. Тогда координатами вектора AB с началом в точке A с координатами (x1; y1) и концом в точке B с координатами (x2; y2) является разность соответствующих координат конца и начала, то есть (x2 - x1; y2 - y1). Поэтому при создании класса вектора Vector мы можем инициализировать его координатами x и y, представляющими собой его смещение относительно начала координат:

class Vector:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
        
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"

Теперь давайте создадим два вектора и попробуем их сложить:

vector1 = Vector(3, 5)
vector2 = Vector(1, 6)
print(vector1 + vector2)
# Ошибка: TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'

Ожидаемо, но мы столкнулись с ошибкой TypeError, ведь Python не умеет складывать объекты класса Vector. Однако Python предоставляет специальный механизм, называемый перегрузкой операторов, который позволяет определять специальное поведение операторов (таких как +, - или * и другие) для пользовательских классов. Это осуществляется с помощью специальных магических методов, например, для операции сложения используется метод __add__().

Перегрузка оператора сложения

Для сложения двух векторов следует сложить соответствующие координаты этих векторов. То есть суммой вектора с координатами (3; 5) и вектора с координатами (1; 6) должен быть новый вектор с координатами (4; 11).

В классе Vector мы можем реализовать это с помощью метода __add__(), который принимает ссылку на экземпляр класса self и объект other, с которым производиться сложение. В данном случае объект other также является экземпляром класса Vector, поэтому мы можем получить координаты этого вектора как атрибуты этого объекта:

class Vector:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
        
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"
        
    def __add__(self, other: "Vector") -> "Vector":
        new_x = self.x + other.x
        new_y = self.y + other.y

        return Vector(new_x, new_y)

При этом арифметические операции не должны изменять исходные объекты, поэтому внутри метода __add__() мы определяем, как сложить компоненты, и возвращаем новый объект класса Vector.

Теперь, когда Python встретит оператор сложения (+) между двумя объектами класса Vector, он будет искать метод __add__() в левом операнде:

vector1 = Vector(3, 5)
vector2 = Vector(1, 6)
print(vector1 + vector2)
# Вывод: Vector(x=4, y=11)

Благодаря перегрузке, мы можем использовать знакомый и интуитивно понятный оператор + для выполнения специфической для нашего класса операции сложения векторов.

Перегрузка арифметических операторов

Перегрузка арифметических операторов позволяет объектам собственных классов участвовать во всех стандартных арифметических операциях, доступных для встроенных типов данных. То есть мы можем определить не только сложение, но и умножение, нахождение остатка от деления и даже возведение в степень.

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

  • self – ссылка на объект, на котором вызывается метод (левый операнд);
  • other – ссылка на объект, используемый в операции (правый операнд).

Когда вы пишете выражение с оператором, Python автоматически переводит его в вызов метода, где порядок аргументов строго определен. Поэтому рассмотренный ранее метод __add__() в выражении vector1 + vector2 преобразуется в vector1.__add__(vector2), то есть он вызывается именно на левом операнде vector1.

Методы перегрузки арифметических операторов

Метод

Оператор

Операция

Сокращение от

__add__(self, other)

+

Сложение

Addition

__sub__(self, other)

-

Вычитание

Subtraction

__mul__(self, other)

*

Умножение

Multiplication

__truediv__(self, other)

/

Деление

True division

__floordiv__(self, other)

//

Целочисленное деление

Floor division

__mod__(self, other)

%

Остаток от деления

Modulo

__pow__(self, other)

**

Возведение в степень

Power

Для примера давайте определим операцию умножения вектора на число. Для этого нужно умножить каждую координату вектора на это число:

class Vector:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
        
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"
        
    def __mul__(self, scalar: int | float) -> "Vector":
        new_x = self.x * scalar
        new_y = self.y * scalar
        return Vector(new_x, new_y)

Обратите внимание, что необязательно называть второй аргумент именем other. Например, здесь более наглядным будет назвать число, на которое умножается вектор, именем scalar.

Теперь умножим вектор с координатами (2; 7) на число 2:

vector = Vector(2, 7)
print(vector * 2)
# Вывод: Vector(x=4, y=14)

В математике, от перемены мест множителей произведение не меняется, поэтому давайте умножим число 2 на экземпляр класса Vector:

vector = Vector(2, 7)
print(2 * vector)
# Ошибка: TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

И здесь мы столкнёмся с исключением TypeError, ведь Python вызывает метод __mul__() на левом операнде – целом числе встроенного типа int, который не работает с объектами класса Vector.

Однако на самом деле в таком случае Python не сразу вызывает исключение, а пробует вызвать отражённый метод на правом операнде. Такие методы отличаются от тех, которые мы уже рассмотрели, только префиксом «r» (от англ. reflected) в начале.

Отражённые методы перегрузки арифметических операторов

Метод

Оператор

Операция

__radd__(self, other)

+

Сложение (отражённое)

__rsub__(self, other)

-

Вычитание (отражённое)

__rmul__(self, other)

*

Умножение (отражённое)

__rtruediv__(self, other)

/

Деление (отражённое)

__rfloordiv__(self, other)

//

Целочисленное деление (отражённое)

__rmod__(self, other)

%

Остаток от деления (отражённое)

__rpow__(self, other)

**

Возведение в степень (отражённое)

То есть если оператор не определён для левого операнда, то Python сначала ищет отражённый метод с префиксом «r» в правом операнде и только потом вызывает исключение.

И если в классе Vector определить метод __rmul__(), то мы можем умножать как целые, так и вещественные числа объекты этого класса:
 

class Vector:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
        
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"
    
    def __mul__(self, scalar: int | float) -> "Vector":
        new_x = self.x * scalar
        new_y = self.y * scalar
        return Vector(new_x, new_y)
    
    def __rmul__(self, scalar: int | float) -> "Vector":
        return self.__mul__(scalar)


vector = Vector(2, 7)
print(2 * vector)
# Вывод: Vector(x=4, y=14)
print(1.5 * vector)
# Вывод: Vector(x=3.0, y=10.5)

Здесь отражённый метод __rmul__() просто вызывает метод __mul__(), в котором остаётся вся логика умножения вектора на число.

При этом методы с префиксом «r» не заменяют обычные методы, а вызываются только в том случае, если операция не поддерживается левым операндом. Поэтому для поддержки умножения вектора на число всё равно придётся писать обычный метод __mul__().

Также мы уже знаем, что арифметические операторы можно совмещать с оператором присваивания, поэтому Python позволяет определить такие операторы для объектов собственных классов. Для этого достаточно к обычным методам добавить префикс «i» (от англ. in-place).

Методы перегрузки арифметических операторов с присваиванием

Метод

Оператор

Операция

__iadd__(self, other)

+=

Сложение с присваиванием

__isub__(self, other)

-=

Вычитание с присваиванием

__imul__(self, other)

*=

Умножение с присваиванием

__itruediv__(self, other)

/=

Деление с присваиванием

__ifloordiv__(self, other)

//=

Целочисленное деление с присваиванием

__imod__(self, other)

%=

Остаток от деления с присваиванием

__ipow__(self, other)

**=

Возведение в степень с присваиванием

Рассмотренные ранее методы, например, __add__(), создают и возвращают новый объект, оставляя исходный объект неизменным. Однако методы с префиксом «i» изменяют состояние объекта self и возвращают его же.

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

class Vector:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
        
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"
        
    def __isub__(self, other: "Vector") -> "Vector":
        self.x -= other.x
        self.y -= other.y
        return self


vector1 = Vector(10, 6)
vector2 = Vector(5, 2)
vector1 -= vector2
print(vector1)
# Вывод: Vector(x=5, y=4)

Когда Python видит операцию vector1 -= vector2, он вызывает метод vector1.__isub__(vector2), который изменяет исходный объект vector1. Однако, если метод __isub()__ не был определён, Python попытается выполнить операцию через __sub()__, а затем присвоить результат обратно переменной.

Перегрузка операций сравнения

Перегрузка операторов сравнения дает возможность определить, что означает, что один объект равен другому или нет, а также больше или меньше его.

Например, если вы создаете свой собственный класс и не реализуете метод перегрузки оператора проверки на равенство (==), то этот оператор по умолчанию будет вести себя так же, как оператор is, то есть проверять, ссылаются ли две переменные на один и тот же объект в памяти

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

Методы перегрузки операторов сравнения

Метод

Оператор

Операция

Сокращение от

__eq__(self, other)

==

Равенство

Equal

__ne__(self, other)

!=

Неравенство

Not equal

__gt__(self, other)

Строго больше

Greater than

__lt__(self, other)

Строго меньше

Less than

__ge__(self, other)

>=

Больше или равно

Greater or equal

__le__(self, other)

<=

Меньше или равно

Less or equal

Представим класс для денег Money, который хранит сумму amount и валюту currency. Два объекта Money логически равны, если их сумма и валюта совпадают:

class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __eq__(self, other: "Money") -> bool:
        # Проверка равенства количества
        same_amount = self.amount == other.amount 
        # Проверка равенства валюты
        same_currency = self.currency == other.currency  
        return same_amount and same_currency

Метод проверки на равенство __eq__() является одним из самых важных. По умолчанию он наследуется от object и просто проверяет, являются ли два объекта одним и тем же объектом в памяти. Переопределяя его, вы определяете логическое равенство по собственным требованиям:

money1 = Money(100, "RUB")
money2 = Money(100, "RUB")
money3 = Money(150, "USD")
money4 = Money(100, "EUR")

print(money1 == money2) 
# Вывод: True (одинаковая сумма и валюта)
print(money1 == money3) 
# Вывод: False (разная сумма)
print(money1 == money4) 
# Вывод: False (разная валюта)

Если вы определяете __eq__, то вам необязательно определять метод проверки на неравенство __ne__(), так как Python использует следующую логику: money1 != money2 эквивалентно not (money1 == money2):

print(money1 != money2) 
# Вывод: False (одинаковая сумма и валюта)
print(money1 != money3) 
# Вывод: True (разная сумма)
print(money1 != money4) 
# Вывод: True (разная валюта)

Также в классе Money мы можем определить, что один объект больше другого, если у него больше сумма, при условии, что валюта одинакова:

class Money:
    def __init__(self, amount: int | float, currency: str):
        self.amount = amount
        self.currency = currency

    def __lt__(self, other: "Money") -> bool:
        if self.currency != other.currency:
            raise ValueError("Нельзя сравнивать деньги в разных валютах")
        return self.amount < other.amount  # Сравнение по сумме


money1 = Money(100, "RUB")
money2 = Money(200, "RUB")
print(money1 < money2) 
# Вывод: True (100 < 200)

Аналогичным образом определяются и другие операторы сравнения.

Взаимодействие с объектами разных типов данных

Бинарные операторы выполняют операцию над двумя операндами. И если левый операнд self гарантированно является ссылкой на экземпляр разрабатываемого класса, то в качестве правого операнда other может быть передан объект любого типа.

Однако функция isinstance() позволяет проверить тип правого операнда other перед совершением операции внутри магического метода. Если она возвращает True, то операция совершается, но если объект other имеет неподходящий тип, и функция возвращает False, то возможно два варианта:

  • Возврат константы NotImplemented, если есть вероятность, что правый операнд может совершить операцию с помощью отражённого метода. То есть операция является коммутативной и её результат не зависит от порядка операндов.
  • Вызов исключения с помощью ключевого слова raise, если операция не должна быть разрешена, даже если объект other имеет отраженный метод.

Представим класс Score, который хранит количество очков value. Объекты этого класса должны складываться друг с другом, а также с обычными числами:

class Score:
    def __init__(self, value: int | float):
        self.value = value
        
    def __repr__(self) -> str:
        return f"Score({self.value})"

    def __add__(self, other: "Score") -> "Score":
        # Если other - это объект класса Score
        if isinstance(other, Score):
            new_value = self.value + other.value
            return Score(new_value)
        
        # Если other - это число
        elif isinstance(other, (int, float)):
            new_value = self.value + other
            return Score(new_value)

        return NotImplemented
    
    def __radd__(self, other: "Score") -> "Score":
        return self.__add__(other)

Проверка типа объекта other позволяет реализовать как сложение двух объектов класса Score:

score1 = Score(10)
score2 = Score(5)
print(score1 + score2)
# Вывод: Score(15)

Так и сложение объекта этого класса с числом:

print(score2 + 25)
# Вывод: Score(30)

При этом отражённый метод __radd__() возвращает метод __add__(), так как сложение, как и умножение, коммутативно, то есть порядок слагаемых не важен:

print(10 + score1)
# Вывод: Score(20)

Если объект other не является ни объектом класса Score, ни числом, то возвращается константа NotImplemented, указывающая Python не сразу вызывать исключение, а вызвать отражённый метод на правом операнде. Поэтому сложение объекта класса Score со строкой приведёт к исключению TypeError:

print(score1 + "12")
# Вывод: TypeError: unsupported operand type(s) for +: 'Score' and 'str'

Однако это исключение возникает после того, как была сделана попытка вызвать отражённый метод __radd__() на объекте класса str.

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

Например, запретим операцию сложения для любого объекта, не являющегося экземпляром класса Score:

class Score:
    def __init__(self, value: int | float):
        self.value = value
        
    def __repr__(self) -> str:
        return f"Score({self.value})"

    def __add__(self, other: "Score") -> "Score":
        if isinstance(other, Score):
            new_value = self.value + other.value
            return Score(new_value)
        raise TypeError(f"Нельзя сложить Score с типом {type(other)}")
    
    def __radd__(self, other: "Score") -> "Score":
        return self.__add__(other)

Теперь исключение вызывается сразу же на объекте класса Score, а также выводится именно то сообщение об исключении, которое мы написали:

score1 = Score(10)
print(score1 + 2)
# Вывод: TypeError: Нельзя сложить Score с типом <class 'int'>

Примеры

Пример 1. Управление временем

Класс Duration описывает отрезок времени в минутах (например, длительность фильма или перерыва) с помощью атрибута minutes, и обеспечивает вычитание одного времени из другого, выполнение целочисленного деления (сколько полных раз умещается одно время в другом) и проверку на равенство:

class Duration:
    """Описывает отрезок времени в минутах."""
    
    def __init__(self, minutes: int):
        """Конструктор класса Duration.
        
        Параметры:
            minutes: Количество минут в отрезке времени.
        """
        self.minutes = minutes
        
    def __str__(self) -> str:
        """Пользовательское описание объекта."""
        return f"{self.minutes} мин."

    def __sub__(self, other: "Duration") -> "Duration":
        """Перегрузка оператора вычитания (-)."""
        if not isinstance(other, Duration):
            return NotImplemented
            
        new_minutes = self.minutes - other.minutes
        return Duration(new_minutes)

    def __floordiv__(self, other: "Duration") -> int:
        """Перегрузка оператора целочисленного деления (//)."""
        if not isinstance(other, Duration) or other.minutes == 0:
            return NotImplemented
        
        return self.minutes // other.minutes
    
    def __eq__(self, other: object) -> bool:
        """Перегрузка оператора равенства (==)."""
        if isinstance(other, Duration):
            return self.minutes == other.minutes
        
        return NotImplemented


film = Duration(120)  # Длительность фильма
trailer = Duration(5)  # Длительность трейлера
break_time = Duration(15)  # Длительность перерыва

# Вычитание (-)
remaining = film - break_time
print(f"{film} - {trailer} - {break_time} = {film - trailer - break_time}")

# Целочисленное деление (//)
print(f"{film} // {trailer} = {film // trailer} мин.")

# Равенство (==)
duration1 = Duration(60)
duration2 = Duration(60)
print(f"{film} == {trailer}? {film == trailer}")
print(f"{film} != {break_time}? {film != break_time}")

Вывод:

120 мин. - 5 мин. - 15 мин. = 100 мин.
120 мин. // 5 мин. = 24
120 мин. == 5 мин.? False
120 мин. != 15 мин.? True

Пример 2. Управление временем доставки

Класс DeliveryTime представляет время доставки в часах с помощью атрибута hours, и поддерживает добавление задержки и проверку на соблюдение временного лимита с по мощью операций сложения с присваиванием (+=) и сравнения на меньше или равно (<=). При этом второй операнд может быть как экземпляром этого класса, так и целым или вещественным числом:

class DeliveryTime:
    """Описывает время доставки в часах."""
    
    def __init__(self, hours: float):
        """Конструктор класса DeliveryTime.
        
        Параметры:
            hours: Время в часах.
        """
        self.hours = hours
        
    def __str__(self):
        """Пользовательское описание объекта"""
        return f"{self.hours} ч."
        
    def __iadd__(self, other: "DeliveryTime") -> "DeliveryTime":
        """Перегрузка оператора сложения с присваиванием (+=)."""
        if isinstance(other, DeliveryTime):
            self.hours += other.hours
        elif isinstance(other, (int, float)):
            self.hours += other # Разрешаем добавление числа (часов)
        
        return NotImplemented

    def __le__(self, other: "DeliveryTime") -> bool:
        """Перегрузка оператора меньше или равно (<=)."""
        if isinstance(other, DeliveryTime):
            return self.hours <= other.hours
        if isinstance(other, (int, float)):
            return self.hours <= other # Сравнение с числом (лимитом в часах)
        
        return NotImplemented


standard_time = DeliveryTime(24.0)  # Стандартное время доставки
delay = DeliveryTime(3.5) # Задержка
limit = 48  # Лимит как целое число

# Сложение с присваиванием (+=)
delivery = standard_time
delivery += delay
print(f"Время доставки с учетом задержки: {delivery}")
delivery += 1.0 # Добавляем еще час (целое число)
print(f"Время доставки после еще часа: {delivery}")

# Сравнение (<=)
print(f"Время доставки <= {limit} часов? {delivery <= limit}")
print(f"Время доставки <= 25 часов? {delivery <= 25}")

Вывод:

Время доставки с учетом задержки: 27.5 ч.
Время доставки после еще часа: 28.5 ч.
Время доставки <= 48 часов? True
Время доставки <= 25 часов? False

Пример 3. Стоимость актива на бирже

Класс Price описывает стоимость акции или любого другого актива на бирже, которая имеет целую (рубли) и дробную (копейки) части. Так как использование стандартных вещественных чисел float может привести к ошибкам округления, то рубли и копейки хранятся отдельно в соответствующих атрибутах rubles и kopecks. Однако для расчётов и сравнения вся стоимость переводится в копейки _total_kopecks, а рубли и копейки вычисляются в методе __init__():

class Price:
    """Описывает стоимость актива."""
    
    def __init__(self, rubles: int = 0, kopecks: int = 0):
        """Конструктор класса Price. 
        Нормализует копейки, чтобы их было не больше 99.
        
        Параметры:
            rubles: Часть стоимости актива в рублях.
            kopecks: Часть стоимости актива в копейках.
        """
        # Вся стоимость в копейках
        self._total_kopecks = rubles * 100 + kopecks  
        
        self.rubles = self._total_kopecks // 100  # Целая часть - это рубли
        self.kopecks = self._total_kopecks % 100  # Остаток - это копейки
        
    def __str__(self):
        """Пользовательское описание объекта."""
        return f"{self.rubles} руб. {self.kopecks} коп."

    def __add__(self, other: "Price") -> "Price":
        """Перегрузка оператора сложения (+)."""
        if not isinstance(other, Price):
            return NotImplemented
        
        total_kopecks = self._total_kopecks + other._total_kopecks
        # Возвращаем новый объект, используя общее количество копеек
        return Price(kopecks=total_kopecks)

    def __sub__(self, other: "Price") -> "Price":
        """Перегрузка оператора вычитания (-)."""
        if not isinstance(other, Price):
            return NotImplemented
        
        total_kopecks = self._total_kopecks - other._total_kopecks
        # Стоимость не может быть отрицательной
        if total_kopecks < 0:
            total_kopecks = 0
        return Price(kopecks=total_kopecks)

    def __gt__(self, other: "Price") -> bool:
        """Перегрузка строго больше (>). 
        Сравнивает по общему количеству копеек.
        """
        if not isinstance(other, Price):
            return NotImplemented  
          
        return self._total_kopecks > other._total_kopecks

    def __eq__(self, other: object) -> bool:
        """Перегрузка оператора равенства (==). 
        Сравнивает количество рублей и копеек.
        """
        if not isinstance(other, Price):
            return NotImplemented  
        
        return self.rubles == other.rubles and self.kopecks == other.kopecks


price1 = Price(rubles=52, kopecks=40) 
price2 = Price(rubles=178, kopecks=87)
price3 = Price(rubles=1024, kopecks=20)

# Сложение (+)
print(f"{price1} + {price2} = {price1 + price2}")
print(f"{price2} + {price3} = {price2 + price3}", end="\n\n")
# Проверка: 5.50 + 11.50 = 17.00

# Вычитание (-)
print(f"{price2} - {price1} = {price2 - price1}")
print(f"{price3} - {price1} = {price3 - price1}", end="\n\n")

# Проверка на равенство (==)
price4 = price2
print(f"{price1} == {price2}? {price1 == price2}")
print(f"{price4} == {price2}? {price4 == price2}", end="\n\n")

# Сравнение (>)
print(f"{price1} > {price2}? {price1 > price2}")
print(f"{price2} > {price3}? {price2 > price3}")

Вывод:

52 руб. 40 коп. + 178 руб. 87 коп. = 231 руб. 27 коп.
178 руб. 87 коп. + 1024 руб. 20 коп. = 1203 руб. 7 коп.

178 руб. 87 коп. - 52 руб. 40 коп. = 126 руб. 47 коп.
1024 руб. 20 коп. - 52 руб. 40 коп. = 971 руб. 80 коп.

52 руб. 40 коп. == 178 руб. 87 коп.? False
178 руб. 87 коп. == 178 руб. 87 коп.? True

52 руб. 40 коп. > 178 руб. 87 коп.? False
178 руб. 87 коп. > 1024 руб. 20 коп.? False

Итоги

  • Перегрузка операторов – это механизм, который позволяет определить поведение стандартных операторов (например, +, - или *) для пользовательских классов.
  • Магические методы перегрузки операторов переопределяют арифметические операторы, включая операторы с присваиванием, а также операторы сравнения.
  • Если оператор не определён для левого операнда, то Python сначала ищет отражённый метод с префиксом «r» в правом операнде и только потом вызывает исключение.
  • Если тип левого операнда проверяется перед совершением операции, то если он не поддерживает эту операцию, то можно явно вернуть константу NotImplemented для вызова отражённого метода или вызвать исключение с помощью ключевого слова raise.

Задания для самопроверки

1. Когда Python вызывает отражённые методы перегрузки?

Если оператор не определён для левого операнда, Python пытается вызвать отражённый метод в правом операнде.

2. Создайте класс для времени Time, который инициализируется одним атрибутом total_seconds для общего количества секунд. Перегрузите оператор * (умножение) так, чтобы он умножал total_seconds на число, переданное в качестве правого операнда, и возвращал новый объект Time. Для удобного вывода информации об объекте определите метод __str__(). Выведите на экран результат операции Time(10) * 3.

class Time:
    def __init__(self, total_seconds: int):
        self.total_seconds = total_seconds

    def __str__(self) -> str:
        return f"Time({self.total_seconds})"

    def __mul__(self, other: int) -> "Time":
        if not isinstance(other, (int, float)):
            return NotImplemented

        new_value = self.total_seconds * other
        return Time(int(new_value))


print(Time(10) * 3)
# Вывод: Time(30)

3. Для класса Time из предыдущего задания реализуйте метод отражённого умножения __rmul__() для умножения числа на объект класса Time. Выведите на экран результат операции 4 * Time(15).

class Time:
    def __init__(self, total_seconds: int):
        self.total_seconds = total_seconds

    def __str__(self) -> str:
        return f"Time({self.total_seconds})"

    def __mul__(self, other: int) -> "Time":
        if not isinstance(other, (int, float)):
            return NotImplemented

        new_value = self.total_seconds * other
        return Time(int(new_value))
    
    def __rmul__(self, other: int) -> "Time":
        # Логика совпадает с прямым умножением
        return self.__mul__(other)


print(4 * Time(15))
# Вывод: Time(60)

4. Создайте класс для товаров Item, который инициализируется атрибутами name для имени и price для цены. Перегрузите оператор == (равенство). Два объекта Item считаются равными, если их атрибуты name и price совпадают. Выведите на экран результат сравнения Item("Атлас", 320) с Item("Атлас", 320).

class Item:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Item):
            return NotImplemented
        
        return self.name == other.name and self.price == other.price


print(Item("Атлас", 320) == Item("Атлас", 320))
# Вывод: True

5. Для класса Item из предыдущего задания перегрузите оператор < (строго меньше). Один товар считается меньше другого, если его цена price строго меньше. Выведите на экран результат сравнения Item("Блокнот", 500) с Item("Текстовыделитель", 60).

class Item:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Item):
            return NotImplemented

        return self.price < other.price


print(Item("Блокнот", 500) < Item("Текстовыделитель", 60))
# Вывод: False