Перейти к содержанию

Определение операторов

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

Специальные методы

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

Специальные методы имеют для интерпретатора особое значение. Имена специальных методов и их смысл определены создателями языка: создавать новые нельзя, можно только реализовывать существующие. Названия всех специальных методов начинаются и заканчиваются на два подчеркивания.

Пример такого метода — уже знакомый нам __init__. Он предназначен для инициализации экземпляров и автоматически вызывается интерпретатором после создания экземпляра объекта.

Остальные специальные методы также вызываются в строго определенных ситуациях. Большинство из них отвечает за реализацию операторов. Так, например, всякий раз, когда интерпретатор встречает запись вида x + y, он заменяет ее на x.__add__(y), и для реализации сложения нам достаточно определить в классе экземпляра x метод __add__.

class Time:
    def __init__(self, minutes, seconds):
        self.minutes = minutes
        self.seconds = seconds


    def __add__(self, other):
        m = self.minutes + other.minutes
        s = self.seconds + other.seconds
        m += s // 60
        s = s % 60
        return Time(m, s)


    def info(self):
        return '{}:{}'.format(self.minutes, self.seconds)


t1 = Time(5, 50)
print(t1.info())  # 5:50
t2 = Time(3, 20)
print(t2.info())  # 3:20
t3 = t1 + t2
print(t3.info())  # 9:10
print(id(t1), id(t2), id(t3))  # Все объекты разные

Обратите внимание: в методе __add__ мы создаем новый экземпляр с результатом сложения, а не изменяем уже существующий. Для арифметических операторов мы будем поступать так почти всегда, ведь при выполнении z = x + y ни x, ни y изменяться не должны. Должен создаваться новый объект z с результатом операции.

Кстати, именно поэтому в некоторых случаях запись a = a + b отличается от a += b. В первом случае вызывается метод __add__, а во втором — __iadd__, для чисел эти методы работают одинаково, а для списков нет.

Так как объекты класса Time относятся к изменяемым, при вызове __iadd__ должен изменяться сам объект. Давайте добавим этот метод в наш класс и посмотрим, как он работает.

class Time:
    def __init__(self, minutes, seconds):
        self.minutes = minutes
        self.seconds = seconds


    def __add__(self, other):
        m = self.minutes + other.minutes
        s = self.seconds + other.seconds
        m += s // 60
        s = s % 60
        return Time(m, s)


    def __iadd__(self, other):
        m = self.minutes + other.minutes
        s = self.seconds + other.seconds
        m += s // 60
        s = s % 60
        self.minutes = m
        self.seconds = s
        return self

    def info(self):
        return '{}:{}'.format(self.minutes, self.seconds)


t1 = Time(5, 50)
print(t1.info())  # 5:50
t2 = Time(3, 20)
print(t2.info())  # 3:20
print(id(t1))
t1 += t2
print(t1.info())  # 9:10
print(id(t1))  # id объекта не поменяется

Обратите внимание: мы не только изменяем атрибуты объекта в методе __iadd__, но и после всех преобразований возвращаем self — сам объект (если мы ничего не вернем, в переменной будет None).

Другой специальный метод позволяет избавиться от вызовов метода info перед передачей данных в print.

Метод str

Перед выводом аргументов на печать функция print преобразует их в строки с помощью функции str. Но функция str делает это не сама, а вызывает метод __str__ своего аргумента. Так что вызов str(x) эквивалентен x.__str__().

Если мы сейчас попытаемся распечатать экземпляры Time просто с помощью print(t1), получим что-то вроде:

<__main__.Time object at 0x7fa021586f98>

Это сработала реализация метода __str__ по умолчанию из класса object. Дело в том, что при создании класса можно указать так называемый суперкласс, от которого наш класс получит всю функциональность. Такой процесс называется наследованием. Об этом механизме мы поговорим на следующем уроке.

Если суперкласс не указать, по умолчанию наследуется класс object, содержащий некоторую базовую функциональность, в том числе метод __str__.

Если мы определим в своем классе собственный метод __str__, он заменит тот, что был унаследован от object.

class Time:
    def __init__(self, minutes, seconds):
        self.minutes = minutes
        self.seconds = seconds


    def __add__(self, other):
        m = self.minutes + other.minutes
        s = self.seconds + other.seconds
        m += s // 60
        s = s % 60
        return Time(m, s)


    def __str__(self):
        return '{}:{}'.format(self.minutes, self.seconds)


t1 = Time(5, 50)
print(t1)  # 5:50
t2 = Time(3, 20)
print(t2)  # 3:20
t3 = t1 + t2
print(t3)  # 9:10

Метод repr

Давайте проведем эксперимент: создадим несколько объектов типа Time, положим их в список, а затем попытаемся вывести его на печать.

li = [Time(5, 50), Time(3, 20)]
print(li)

[<__main__.Time object at 0x7ff15c76e4e0>, <__main__.Time object at 0x7ff15c76e550>]

Но почему такое произошло? Мы же добавили метод для преобразования к строке! Оказывается, кроме метода __str__, который предназначен для выдачи информации об экземпляре для пользователей в «человеческом» виде, часто определяется метод __repr__. Для метода __repr__, как и для __len__, есть функция, явно вызывающая этот метод у объекта.

Функция repr предназначена для выдачи полной информации об объекте для программиста. Она часто применяется при отладке. Поскольку «сырой» вывод списка обычно не предназначен для пользователя, он вызывает у объектов не метод __str__, а метод __repr__. Для нашего класса Time этот метод мог бы выглядеть так:

class Time:
    #методы __init__, __add__, __str__ 

    def __repr__(self):
        return 'Time({}, {})'.format(self.minutes, self.seconds)


t1 = Time(5, 50)
print(t1)  # 5:50
print(repr(t1))  # Time(5, 50)

Как видно, здесь метод __repr__ выдает строку, которую можно скопировать и вставить в исходный код на Python, чтобы получить выражение, которое заново сконструирует такой же объект.

Класс «Вектор на плоскости»

Вектор на плоскости

Двумерные векторы — очень полезный и важный геометрический объект. Векторы любой нужной размерности уже есть в библиотеке Numpy, но, если бы мы захотели реализовать двумерный вектор самостоятельно, можно было бы сделать это, например, так:

class MyVector:
    def __init__(self, x, y):
        self.x = x
        self.y = y


    def __add__(self, other):
        return MyVector(self.x + other.x, self.y + other.y)


    def __sub__(self, other):
        return MyVector(self.x - other.x, self.y - other.y)


    def __mul__(self, other):
        return MyVector(self.x * other, self.y * other)

    def __rmul__(self, other):
        return MyVector(self.x * other, self.y * other)


    def __str__(self):
        return 'MyVector({}, {})'.format(self.x, self.y)


v1 = MyVector(-2, 5)
v2 = MyVector(3, -4)
v_sum = v1 + v2
print(v_sum)  # MyVector(1, 1)
v_mul = v1 * 1.5
print(v_mul)  # MyVector(-3.0, 7.5)
v_rmul = -2 * v1
print(v_rmul)  # MyVector(4, -10)

В этом примере определены методы __add__ и __sub__ для реализации классических операций сложения и вычитания векторов. Метод __mul__ реализует операцию умножения вектора на число, а метод __rmul__ — операцию умножения числа на вектор. Для преобразования в строку используется уже знакомый нам метод __str__.

Другие специальные методы

Специальных методов слишком много, чтобы рассмотреть их все на этом уроке. Мы приведем лишь небольшой их список.

Метод Описание
__add__(self, other) Сложение (x + y). Будет вызвано: x.__add__(y)
__sub__(self, other) Вычитание (x - y)
__mul__(self, other) Умножение (x * y)
__truediv__(self, other) Деление (x / y)
__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))
__radd__(self, other) Сложение (y + x). Будет вызвано: y.__radd__(x)
__rsub__(self, other) Вычитание (y - x)
__lt__(self, other) Сравнение (x < y). Будет вызвано: x.__lt__(y)
__eq__(self, other) Сравнение (x == y). Будет вызвано: x.__eq__(y)
__len__(self) Возвращение длины объекта
__getitem__(self, key) Доступ по индексу (или ключу)
__call__(self[, args...]) Вызов экземпляра класса как функции

Однако найти полную документацию по специальным методам в Интернете сравнительно легко. Если вам нужно реализовать тот или иной оператор, для начала поищите соответствующий ему специальный метод на втором листе вот этой шпаркалки

Если вы не нашли необходимой информации, рекомендуем очень подробную статью с длинным и обстоятельным описанием

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


Задачи

Задача 1

Марина заботится о здоровом питании. Она попросила вас написать класс FoodInfo, экземпляры которого будут описывать пищевую ценность продуктов, а при сложении — возвращать новый экземпляр, описывающий суммарную пищевую ценность его составляющих. Интерфейс класса: - fi = FoodInfo(proteins, fats, carbohydrates) — инициализировать экземпляр заданным количеством белков, жиров и углеводов. Все три параметра определяются весом в граммах (целым числом). - fi.get_proteins() — вернуть количество белков. - fi.get_fats()— вернуть количество жиров. -fi.get_carbohydrates()— вернуть количество углеводов. -fi.get_kcalories()— рассчитать число килокалорий в пище по формуле **(4 * белки + 9 * жиры + 4 * углеводы).** -fi_sum = fi1 + fi2— результат сложенияfi_sumдолжен быть новым объектомFoodInfo`, описывающим суммарную пищевую ценность продуктов fi1 + fi2. fi1 и fi2 не должны измениться.

Тест 1

Ввод

# Ваш код

food1 = FoodInfo(100, 100, 100)
food2 = FoodInfo(50, 60, 70)
food3 = food1 + food2
print(food1.get_proteins(), food1.get_fats(),
      food1.get_carbohydrates(), food1.get_kcalories())
print(food2.get_proteins(), food2.get_fats(),
      food2.get_carbohydrates(), food2.get_kcalories())
print(food3.get_proteins(), food3.get_fats(),
      food3.get_carbohydrates(), food3.get_kcalories())

Вывод

100 100 100 1700
50 60 70 1020
150 160 170 2720

Тест 2

Ввод

# Ваш код

food1 = FoodInfo(1, 2, 3)
food2 = FoodInfo(10, 20, 30)

food3 = food1 + food2

print(food3.get_proteins(), food3.get_fats(),
      food3.get_carbohydrates(), food3.get_kcalories()) 

Вывод

11 22 33 374

Тест 3

Ввод

# Ваш код

food1 = FoodInfo(1, 2, 3)
food2 = FoodInfo(10, 20, 30)

food3 = food1 + food2
food4 = food2 + food1

print(food3.get_proteins(), food3.get_fats(),
      food3.get_carbohydrates(), food3.get_kcalories())
print(food4.get_proteins(), food4.get_fats(),
      food4.get_carbohydrates(), food4.get_kcalories())

Вывод

11 22 33 374
11 22 33 374

Задача 2

Напишите класс ReversedList, который будет при инициализации экземпляра принимать список и реализовывать доступ к элементам этого списка в обратном порядке. - rl = ReversedList(lst) — создание обратного списка. - len(rl) — число элементов в обратном списке. - rl[i] — доступ к элементам в обратном порядке. rl[0] — последний элемент первоначального списка, rl[1] — предпоследний и так далее.

Тест 1

Ввод

# Ваш код

rl = ReversedList([10, 20, 30])
for i in range(len(rl)):
    print(rl[i])

Вывод

30
20
10

Тест 2

Ввод

# Ваш код

rl = ReversedList([])
print(len(rl))

Вывод

0

Тест 3

Ввод

# Ваш код

rl = ReversedList([10])
print(len(rl))
print(rl[0])

Вывод

1
10

Функцию len переопределять не нужно, используйте специальный метод.

Задача 3

Реализуйте класс SquareFunction, экземпляры которого при инициализации получают коэффициенты a, b, c. При вызове объекта как функции с аргументом x должно возвращаться значение выражения ax2 + bx + c.

Тест 1

Ввод

# Ваш код

sf = SquareFunction(1, 0, 0)
print(sf(-2))
print(sf(-1))
print(sf(-0))
print(sf(1))
print(sf(2))
print(sf(10))

Вывод

4
1
0
1
4
100

Тест 2

Ввод

# Ваш код

sf = SquareFunction(1, 2, 1)
print(sf(-2))
print(sf(-1))
print(sf(-0))
print(sf(1))
print(sf(2))
print(sf(10))

Вывод

1
0
1
4
9
121

Тест 3

Ввод

# Ваш код

sf = SquareFunction(0, 0, 1)
print(sf(-2))
print(sf(-1))
print(sf(-0))
print(sf(1))
print(sf(2))
print(sf(10))

Вывод

1
1
1
1
1
1

Задача 4

Реализуйте класс Date, экземпляры которого при инициализации принимают месяц и день. При вычитании дат (d1 - d2) должно возвращаться число дней между d1 и d2. Число дней должно быть: - равно нулю, если d1 и d2 — одна и та же дата - больше нуля, если d1 позже d2 - меньше нуля, если d1 раньше d2

Считайте, что все даты указаны в пределах одного и того же не високосного года (в феврале 28 дней).

Тест 1

Ввод

# Ваш код

jan5 = Date(1, 5)
jan1 = Date(1, 1)

print(jan5 - jan1)
print(jan1 - jan5)
print(jan1 - jan1)
print(jan5 - jan5)

Вывод

4
-4
0
0

Тест 2

Ввод

# Ваш код

mar5 = Date(3, 1)
jan1 = Date(1, 1)

print(mar5 - jan1)
print(jan1 - mar5)
print(jan1 - jan1)
print(mar5 - mar5)

Вывод

59
-59
0
0

Задача 5

Определите класс Point. При инициализации экземпляру передаются координаты x и y. При сравнении двух экземпляров оператор == должен возвращать True, если координаты точек равны, и False — если нет. При сравнении оператором != должно возвращаться True, если координаты точек не равны, и False — если равны.

Тест 1

Ввод

# Ваш код

p1 = Point(1, 2)
p2 = Point(5, 6)

if p1 == p2:
    print("Equal True")
else:
    print("Equal False")

if p1 != p2:
    print("Not equal True")
else:
    print("Not equal False")

Вывод

Equal False
Not equal True

Тест 2

Ввод

# Ваш код

p1 = Point(0, 0)
p2 = Point(0, 0)

if p1 == p2:
    print("Equal True")
else:
    print("Equal False")

if p1 != p2:
    print("Not equal True")
else:
    print("Not equal False")

Вывод

Equal True
Not equal False

Тест 3

Ввод

# Ваш код

p1 = Point(0, 10)
p2 = Point(0, 0)

if p1 == p2:
    print("Equal True")
else:
    print("Equal False")

if p1 != p2:
    print("Not equal True")
else:
    print("Not equal False")

Вывод

Equal False
Not equal True

Задача 6

Периодически в некоторых задачах возникает надобность в разреженных массивах. Разреженный массив характеризуется тем, что подавляющее большинство значений в нём равны нулю, поэтому можно хранить только ненулевые значения. Это позволяет создавать разреженные массивы очень большого размера без лишних затрат памяти. Вам необходимо реализовать класс разреженного массива SparseArray. - arr = SparseArray() — создание пустого разреженного массива. - arr[i] = value — записать значение в массив. - arr[i] — прочитать значение из массива.

Тест 1

Ввод

# Ваш код

arr = SparseArray()
arr[1] = 10
arr[8] = 20
for i in range(10):
    print('arr[{}] = {}'.format(i, arr[i]))

Вывод

arr[0] = 0
arr[1] = 10
arr[2] = 0
arr[3] = 0
arr[4] = 0
arr[5] = 0
arr[6] = 0
arr[7] = 0
arr[8] = 20
arr[9] = 0

Тест 2

Ввод

# Ваш код

arr = SparseArray()
arr[10] = 123
for i in range(8, 13):
    print('arr[{}] = {}'.format(i, arr[i]))

Вывод

arr[8] = 0
arr[9] = 0
arr[10] = 123
arr[11] = 0
arr[12] = 0

Тест 3

Ввод

# Ваш код

def print_elem(array, ind):
    print('arr[{}] = {}'.format(ind, array[ind]))


arr = SparseArray()
index = 1000000000
arr[index] = 123

print_elem(arr, index - 1)
print_elem(arr, index)
print_elem(arr, index + 1)

Вывод

arr[999999999] = 0
arr[1000000000] = 123
arr[1000000001] = 0

Задача 7

Реализуйте класс Polynomial для вычисления значений многочленов при заданных x, а также для сложения многочленов. - poly = Polynomial(coefficients) — создать новый многочлен с коэффициентами coefficients. coefficients[0] — свободный член, coefficients[1] — множитель при x1, coefficients[2] — множитель при x2, и так далее. - y = poly(x) — вычислить значение многочлена в точке x. - y = c0 + c1 x1 + c2 x2 + ... + cn xn - poly_sum = poly1 + poly2 — результат сложения двух многочленов должен быть экземляром Polynomial и содержать коэфициенты, которые получаются в результате сложения коэффициентов poly1 и poly2 при одинаковых степенях x.

Тест 1

Ввод

# Ваш код

poly = Polynomial([10, -1])
print(poly(0))
print(poly(1))
print(poly(2))

Вывод

10
9
8

Тест 2

Ввод

# Ваш код

poly1 = Polynomial([0, 0, 1])
print(poly1(-2))
print(poly1(-1))
print(poly1(0))
print(poly1(1))
print(poly1(2))
print()

poly2 = Polynomial([0, 0, 2])
print(poly2(-2))
print(poly2(-1))
print(poly2(0))
print(poly2(1))
print(poly2(2))
print()

poly3 = poly1 + poly2
print(poly3(-2))
print(poly3(-1))
print(poly3(0))
print(poly3(1))
print(poly3(2))
print()

Вывод

4
1
0
1
4

8
2
0
2
8

12
3
0
3
12

Тест 3

Ввод

# Ваш код

poly1 = Polynomial([0, 1])
poly2 = Polynomial([10])
poly3 = poly1 + poly2
poly4 = poly2 + poly1

print(poly3(-2), poly4(-2))
print(poly3(-1), poly4(-1))
print(poly3(0), poly4(0))
print(poly3(1), poly4(1))
print(poly3(2), poly4(2))

Вывод

8 8
9 9
10 10
11 11
12 12

Задача 8

Программист Николай, изучая различные структуры данных, решил погрузиться в теорию очередей, построенных на базе списков Python. Напомним, что очередь – это такая структура данных, работа с которой определяется принципом FIFO (First In First Out). Наблюдать этот принцип можно у кассы в магазине. Если, конечно, никто не нарушает правила 😊 Николай предлагает Вам реализовать класс Queue, инкапсулирующий, то есть моделирующий работу, очередь. Однако, Николай не ищет легких путей и хочет усложнить Вам работу: очередь может быть инициализирована различным числом элементов. Например, - q = Queue(1, 2, 3, 4, 5)– создаёт очередь [1 -> 2 -> 3 -> 4 -> 5] - q1 = Queue(1, 2, 3) – очередь [1 -> 2 -> 3]. Но всё же есть гарантия того, что для инициализации передается хотя бы один параметр. Класс Queue реализует следующие методы: - append(*values) – добавляет несколько значений в конец очереди (как минимум одно). - copy() – создаёт копию данной очереди, то есть возвращает новую очередь, полностью аналогичную исходной. - pop() – вытаскивает и возвращает первый элемент очереди, при этом этот элемент из очереди удаляется. Если очередь пуста, то возвращает None. - extend(queue) – расширяет данную очередь другой, то есть приклеивает вторую очередь к первой. - next() – возвращает новую очередь, начинающуюся со второго элемента текущей.

Также требуется реализовать следующие операторы и встроенные функции: - queue1 + queue2 – склейка очередей создаёт новую увеличенную очередь. - queue1+ = queue2 – расширяет первую очередь второй. - queue1 == queue2 – проверяет очереди на равенство всех элементов. Возвращает True или False. - queue >> N – создаёт новую очередь без первых N (вышедших) элементов. В случае, когда N превышает количество элементов очереди, следует вернуть пустую очередь. - str(queue) – приводит очередь к строке вида [q1 -> q2 -> q3 -> ... -> qn]. Пустая или ошибочная очередь – это [] - next(queue) – аналогичное действие методу next().

Тест 1

Ввод

# Ваш код

q1 = Queue(1, 2, 3)
print(q1)
q1.append(4, 5)
print(q1)
qx = q1.copy()
print(qx.pop())
print(qx)
q2 = q1.copy()
print(q2)
print(q1 == q2, id(q1) == id(q2))
q3 = q2.next()
print(q1, q2, q3, sep = '\n')
print(q1 + q3)
q3.extend(Queue(1, 2))
print(q3)
q4 = Queue(1, 2)
q4 += q3 >> 4
print(q4)
q5 = next(q4)
print(q4)
print(q5)

Вывод

[1 -> 2 -> 3]
[1 -> 2 -> 3 -> 4 -> 5]
1
[2 -> 3 -> 4 -> 5]
[1 -> 2 -> 3 -> 4 -> 5]
True False
[1 -> 2 -> 3 -> 4 -> 5]
[1 -> 2 -> 3 -> 4 -> 5]
[2 -> 3 -> 4 -> 5]
[1 -> 2 -> 3 -> 4 -> 5 -> 2 -> 3 -> 4 -> 5]
[2 -> 3 -> 4 -> 5 -> 1 -> 2]
[1 -> 2 -> 1 -> 2]
[1 -> 2 -> 1 -> 2]
[2 -> 1 -> 2]