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

Перегрузка операторов в 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()
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 объекта.

В Python кроме метода __str__() есть схожий с ним по поведению, но более "низкоуровневый" метод __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__() перегружает не функцию print(), а функцию str(). Функция же print() так устроена, что сама вызывает str() для своих аргументов.

В Python есть встроенная функция 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__() – деление. В классе код этих методов должен выполнять увеличение или уменьшение количества снежинок на число n или в n раз. Метод __truediv__() перегружает обычное (/), а не целочисленное (//) деление. Однако пусть в методе происходит округление значения до целого числа.

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

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

Курс с примерами решений практических работ: android-приложение, pdf-версия.

Создано

Обновлено

Комментарии

class Qwe:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def __call__(self, newWidth, newHeight):
        self.width = newWidth
        self.height = newHeight
    def __str__(self):
        return "%s" % self.width
        return "%s" % self.height
q = Qwe(4, 7)
w = Qwe(9, 1)
 
q(2,5)
w(3,9)
 
print (q, w)

Ответ на от Гость

Потому что оператор return осуществляет выход из функции. В данном случае выражение return "%s" % self.height никогда не выполняется. Как вариант:

... 
    def __str__(self):
        s = str(self.width) + " " + str(self.height)
        return s
...