Перегрузка операторов в языке Python позволяет изменять стандартное поведение операторов через внедрение специальных методов в классы. Эти методы определены с помощью двойного подчеркивания в начале и в конце их имен.

Под операторами подразумеваются не только такие символы, как +, -, *, /, выполняющие арифметические операции, но также операции, связанные с особенностями синтаксиса языка, которые включают создание объектов, вызов объекта в виде функции, доступ к элементам по индексу и вывод данных, и многое другое.

Ранее мы прибегали к некоторым методам перегрузки операторов. Среди них:

  • __init__() – является конструктором объекта, его вызов происходит в момент создания экземпляра
  • __del__() – выступает как деструктор объекта, вызывается при его удалении
  • __str__() – отвечает за преобразование объекта в строку, вызывается при передаче объекта в функции print() и str()
  • __add__() – реализует перегрузку оператора сложения, применяется при участии объекта в операции сложения в роли левого операнда
  • __setattr__() – вызывается, когда атрибут объекта получает новое значение

Количество других методов перегрузки операторов в Python велико. Мы рассмотрим еще несколько самых распространенных в этом уроке.

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

Перегрузка операторов дает возможность создавать пользовательские классы, полностью совместимые с встроенными классами Python, поскольку все встроенные типы данных являются классами. Это приводит к унификации интерфейсов объектов. Если ваш класс предусматривает обращение к его элементам по индексу, к примеру, a[0], это вполне осуществимо.

Будем рассматривать класс-агрегат B, содержащий объекты класса A в списке:

class A:
    def __init__(self, arg):
        self.arg = arg
    def __str__(self):
        return str(self.arg)

class B:
    def __init__(self, *args):
        self.aList = []
        for i in args:
            self.aList.append(A(i))

group = B(5, 10, 'abc')

Для извлечения элемента из списка мы можем использовать индексирование поля aList:

print(group.aList[1])

Тем не менее, гораздо удобнее извлекать элемент непосредственно из объекта по индексу:

class B:
    def __init__(self, *args):
        self.aList = []
        for i in args:
            self.aList.append(A(i))
    def __getitem__(self, i):
        return self.aList[i]

group = B(5, 10, 'abc')
print(group.aList[1]) # выведет 10
print(group[0]) # 5
print(group[2]) # abc

Это делает объекты класса B аналогичными объектам встроенных в Python классов-последовательностей, таких как списки или строки. Метод __getitem__() реализует извлечение элемента по индексу, вызывается, когда применяется операция индексирования: объект[индекс].

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

a = A()
a()
a(3, 4)

Метод __call__() автоматически активируется при попытке вызова объекта как функции. Например, здесь во второй строке будет вызван метод __call__() некогоКласса:

объект = некийКласс()
объект([возможные аргументы])

Пример:

class Changeable:
     def __init__(self, color):
          self.color = color
     def __call__(self, newcolor):
          self.color = newcolor
     def __str__(self):
          return "%s" % self.color

canvas = Changeable("green")
frame = Changeable("blue")

canvas("red")
frame("yellow")

print(canvas, frame)

В данном примере класс при создании своих объектов определяет их цвет. Чтобы изменить цвет, мы можем использовать объект как функцию, передавая в скобках новый цвет. Метод __call__() автоматически изменит атрибут color объекта.

Помимо метода __str__(), в Python есть аналогичный по функционалу, но более "низкоуровневый" метод __repr__(). Оба они должны возвращать строку.

Если классу присущ только метод __str__(), то при прямом обращении к объекту в интерпретаторе без использования функции print(), он не будет вызван:

>>> class A:
...     def __str__(self):
...         return "This is object of A"
...
>>> a = A()
>>> print(a)
This is object of A
>>> a
<__main__.A instance at 0x7fe964a4cdd0>
>>> str(a)
'This is object of A'
>>> repr(a)
'<__main__.A instance at 0x7fe964a4cdd0>'

Метод __str__() вызывает встроенная функция str() при попытке преобразования объекта в строку. Это значит, что метод __str__() фактически является перегрузкой str(), а не print(). Функция print() вызывает str() для преобразования своих аргументов.

Встроенная функция repr(), как и str(), преобразует объект в строку, но в "сыром" виде. Что это означает, мы рассмотрим на примере:

>>> a = '3 + 2'
>>> b = repr(a)
>>> a
'3 + 2'
>>> b
"'3 + 2'"
>>> eval(a)
5
>>> eval(b)
'3 + 2'
>>> c = "Hello\nWorld"
>>> d = repr(c)
>>> c
'Hello\nWorld'
>>> d
"'Hello\\nWorld'"
>>> print(c)
Hello
World
>>> print(d)
'Hello\nWorld'

Функция eval выполняет строку как программный код, а print() осуществляет переход на новую строку по символу \n. repr() защищает строку от интерпретации, сохраняя ее "сырой". Напомним:

>>> c = "Hello\nWorld"
>>> c # аналог print(repr(c))
'Hello\nWorld'
>>> print(c) # аналог print(str(c))
Hello
World

Однако в большинстве случаев разница между repr() и str() несущественна. Следовательно, удобнее создавать в классах только метод __repr__(), так как он будет использоваться для всех случаев, когда необходимо строковое представление объекта:

>>> class A:
...     def __repr__(self):
...         return "It's obj of A"
...
>>> a = A()
>>> a
It's obj of A
>>> repr(a)
"It's obj of A"
>>> str(a)
"It's obj of A"
>>> print(a)
It's obj of A

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

Практическая работа

Создайте класс Snow по следующему описанию.

Конструктор класса инициализирует поле, представляющее собой количество снежинок в виде целого числа.

Класс реализует методы для перегрузки арифметических операторов: __add__() для сложения, __sub__() для вычитания, __mul__() для умножения и __truediv__() для деления. Эти методы должны увеличивать или уменьшать количество снежинок на заданное число или в указанное число раз. Метод __truediv__() реализует операцию обычного деления (/). Однако резульататы будут округляться до ближайшего целого.

Внесите в класс метод makeSnow(), способный принимать сам объект и число снежинок в строке, и возвращающий строку в формате "*****\n*****\n*****…", где количество снежинок между '\n' соответствует аргументу, переданному этому методу. Количество строк определяется исходя из общего количества снежинок.

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

Вопросы для самопроверки:

  1. Что такое перегрузка операторов в языке Python и как она реализуется в классах?
  2. Что делает метод __getitem__() в классе Python?
  3. В чем разница между методами __str__() и __repr__() при преобразовании объекта в строку?
  4. Как метод __call__() может быть полезен в классе Python?