Перегрузка специальных методов и операторов
Contents
Перегрузка специальных методов и операторов#
Перегрузка специальных методов позволяет достичь многих аспектов полиморфизма.
Приведение к строке. __str__
и __repr__
#
Если попробовать распечатать экземпляр пользовательского класса, то вы получите нечто не очень содержательное.
from numbers import Number
class Quaternion(Number):
def __init__(self, a, b, c, d):
self.a = a
self.b = b
self.c = c
self.d = d
q = Quaternion(1, 2, 3, 4)
print(q)
<__main__.Quaternion object at 0x00000228CEFE1FD0>
При попытке распечатать экземпляр класса Quaternion
мы получили сообщение, из которого никак нельзя узнать значение атрибутов объекта.
Этот класс в примере реализует тип кватернионов. Кватернион \(q\) можно задать четырьмя действительными числами \(a\), \(b\), \(c\) и \(d\). Тогда он определяется как сумма
Сделаем так, чтобы функцией print
на экран выводилось представление кватерниона в такой форме. Для это необходимо объявить специальный метод __str__, который отвечает за приведение объекта к строке (встроенная функция str сначала пытается вызвать этот метод).
class Quaternion(Number):
def __init__(self, a, b, c, d):
self.a = a
self.b = b
self.c = c
self.d = d
def __str__(self):
return f"{self.a} + {self.b}i + {self.c}j + {self.d}k"
q = Quaternion(1, 2, 3, 4)
print(q)
1 + 2i + 3j + 4k
Есть также похожий метод __repr__ (и соответствующая встроенная функция repr), который тоже должен возвращать строку. Разница между __str__
и __repr__
примерное следующая.
Метод
__str__
должен возвращать читабельную для человека строку. Такой метод вызывается функциейprint
.Метод
__repr__
должен возвращать строку, из которой можно было бы восстановить объект. Такой метод может быть полезен для отладки.
Для примера реализуем метод __repr__
, вычисление результата которого воссоздаст исходный объект.
class Quaternion(Number):
def __init__(self, a, b, c, d):
self.a = a
self.b = b
self.c = c
self.d = d
def __str__(self):
return f"{self.a} + {self.b}i + {self.c}j + {self.d}k"
def __repr__(self):
return f"Quaternion(a={self.a}, b={self.b}, c={self.c}, d={self.d})"
q = Quaternion(1, 2, 3, 4)
representation = repr(q)
print(representation)
q_copy = eval(representation)
print(q_copy)
Quaternion(a=1, b=2, c=3, d=4)
1 + 2i + 3j + 4k
Вызов объекта. __call__
#
Определив специальный метод __call__
, можно имитировать функции.
В качестве примера реализуем класс, который позволит вычислять композицию функций одной переменной. Напомним, что композицией двух функций \(f:\mathbb{R} \to \mathbb{R}\) и \(g:\mathbb{r} \to \mathbb{R}\) называется функция \(h = f \circ g:\mathbb{R} \to \mathbb{R}\), значение которой \(h(x) = f(g(x))\) для \(\forall x \in \mathbb{R}\).
Реализация такого класса может выглядеть как-нибудь вот так.
from collections.abc import Callable
import math
class Composition(Callable):
def __init__(self, f, g):
self.f = f
self.g = g
def __call__(self, x):
return self.f(self.g(x))
f = abs
g = math.sin
h = Composition(f, g)
print(h(-math.pi / 2.))
1.0
Конструктор класса Composition
принимает на вход две функции (или вызываемых объекта), запоминает их в атрибуты f
и g
. При вызове объекта (пара круглых скобок после имени объекта), вызывается метод __call__
, который в данном случае возвращает результат вычисления f(g(x))
.
import numpy as np
from matplotlib import pyplot as plt
plt.rc('font', size=18)
X = np.linspace(0, 2*np.pi)
Y1 = [h(x) for x in X]
Y2 = [f(g(x)) for x in X]
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(X, Y1, "r.", label="h(x)")
ax.plot(X, Y2, label="f(g(x))")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
<matplotlib.legend.Legend at 0x1deae7afe50>
Итератор по объекту. __iter__
#
Если ваш элемент класс является контейнером, то может потребоваться возможность пробежаться по его элементам. Для этого необходимо, чтобы встроенная функция iter возвращала итератор по этому объекту. Чтобы это работало у пользовательского класса, необходимо или реализовать оператор извлечения элемента по индексу (см. Операторы контейнеров) или реализовать метод __iter__
, чтобы он возвращал итератор по нему.
Итератор в свою очередь должен поддерживать метод __next__
и метод __iter__
. Последний необходим для того, чтобы итератор сам по себе можно было использовать в цикле for
(выражение for x in iterable
первым шагом вызывает iter(iterable)
) и в схожих контекстах.
В ряде случаев проще всего реализовать метод __iter__
в виде генератора, т.к. они автоматически поддерживают и метод __next__
и метод __iter__
. В качестве примера рассмотрим класс векторов трехмерного пространства и предположим, что мы хотим уметь итерироваться по его координатам.
from collections import abc
class Vector3D(abc.Iterable):
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __iter__(self):
yield self.x
yield self.y
yield self.z
v = Vector3D(42, 3, 13)
for coordinate in v:
print(coordinate, end=" ")
42 3 13
Как видим, метод __iter__
является функцией-генератором. Объект генератора результата вызова __init__()
по очереди возвращает координаты x
, y
и z
.
Теперь, в качестве альтернативы реализуем класс итератора Iterator
по объектам Vector3D
в явном виде. Теперь метод Vector3D.__iter__
возвращает экземпляр класса Iterator
.
from collections import abc
class Vector3D(abc.Iterable):
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __iter__(self):
return Vector3D.Iterator(self)
class Iterator(abc.Iterator):
attributes = "xyz"
def __init__(self, vector):
print("Создан итератор.")
self.vector = vector
self.current = 0
def __iter__(self):
return self
def __next__(self):
try:
current_attribute = self.attributes[self.current]
except IndexError:
raise StopIteration
self.current += 1
return getattr(self.vector, current_attribute)
v = Vector3D(42, 3, 13)
for coordinate in v:
print(coordinate, end=" ")
Создан итератор.
42 3 13
В целях инкапсуляции класс итератора объявлен прямо в теле исходного класса;
Метод
__init__
этого класса принимает на вход экземпляр класса, запоминает значения атрибутов класса и устанавливает значение счетчик равным нулю. Этот счетчик затем используется, чтобы запомнить атрибут, на котором мы остановились в методеnext
;Метод
__next__
возвращает значение следующего атрибута, имя которого получается индексацией из строкиattributes
. Когда символы этой строки заканчиваются, то происходит обращение к символу строки за её пределами, что приводит к возбуждению исключенияIndexError
. Это исключение перехватывается и вместо него возбуждаетсяStopIteration
, что сигнализирует вызывающему коду, что итератор исчерпался;Метод
__iter__
итератора возвращает ссылку на себя, что позволяет использовать его самого в циклеfor
, как в примере ниже.
it = iter(v)
print(it)
for coordinate in it:
print(coordinate, end=" ")
Создан итератор.
<__main__.Vector3D.Iterator object at 0x00000228CE202730>
42 3 13
Перегрузка операторов#
Доступные операторы#
Как и во многих других языках программирования, в python
можно перегружать действие операторов на пользовательские объекты. Все операторы в python
могут быть представлены в форме специальных методов. Например, метод __add__
из этого модуля соответствует оператору +
.
x = 42
y = 7
print(x.__add__(y))
49
Для всех остальных арифметических операторов, есть свои функции и, забегая вперед, специальные методы с такими же именами, но окруженные двойными нижними подчеркиваниями.
Арифметические операторы#
Арифметическое действие |
Операторная форма |
Метод |
---|---|---|
Сложение |
|
|
Вычитание |
|
|
Умножение |
|
|
Обычное деление |
|
|
Целочисленное деление |
|
|
Остаток от деления |
|
|
Возведение в степень |
|
|
Матричное умножение |
|
|
Положительный |
|
|
Арифметическое отрицание |
|
|
Note
Для каждого бинарного оператора существует аналог с префиксом r
(например, __radd__
) и аналог с префиксом i
(например, __iadd__
).
Операнд справа#
Если встречается выражение a + b
, то сначала python
пробует вызвать a.__add__(b)
. Если такой метод у объекта a
не определен или этот метод возвращает NotImplemented, то python
пробует вызвать b.__radd__(a)
. Если и он не определен или возвращает NotImplemented
, то python
возбуждает ошибку TypeError
.
Версия оператора на месте#
Если встречается выражение a += b
, то сначала python
попытается вызвать a = a.__iadd__(b)
. Если такой метод у объекта a
не определен, то python
пробует свести выражение a += b
к выражению a = a + b
, т.е. a = a.__add__(b)
. Далее процедура следует алгоритму, описанному в предыдущем абзаце.
Warning
Методы на месте должны возвращать ссылку на какой-то объект. Для неизменяемых типов это обычно ссылка на новый объект, для изменяемых — ссылка на исходный объект (self
, если вы следуете соглашениям).
Пример#
В качестве самого простого примера реализуем свой тип “изменяемых” целых чисел, который будет поддерживать все формы оператора умножения.
from numbers import Number
class MyMutableInt(Number):
def __init__(self, value):
if not isinstance(value, int):
raise TypeError("Допускаются только целые числа")
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return f"MyMutableInt(value={self.value})"
def __mul__(self, other):
print(f"Умножение {self} на {other}. Объект слева.")
if(isinstance(other, int)):
return MyMutableInt(self.value * other)
if(isinstance(other, MyMutableInt)):
return MyMutableInt(self.value * other.value)
return NotImplemented
def __rmul__(self, other):
print(f"Умножение {other} на {self}. Объект справа.")
return self * other
def __imul__(self, other):
print(f"Умножение {self} на {other} на месте")
if(isinstance(other, int)):
self.value *= other
return self
if(isinstance(other, MyMutableInt)):
self.value *= other.value
return self
return NotImplemented
print(MyMutableInt(3) * MyMutableInt(7))
print(MyMutableInt(42) * 13)
print(42 * MyMutableInt(13))
Умножение 3 на 7. Объект слева.
21
Умножение 42 на 13. Объект слева.
546
Умножение 42 на 13. Объект справа.
Умножение 13 на 42. Объект слева.
546
Умножать экземпляры этого класса на встроенные целые числа можно в любом порядке за счет реализации метода __rmul__
.
Note
Важно обратить внимание, что не все арифметические операции коммутативны, а значит в некоторых из них нельзя в методе с префиксом r
просто поменять операнды местами.
x = MyMutableInt(3)
print(f"Значение {x} по адресу {id(x)}")
x *= 7
print(f"Значение {x} по адресу {id(x)}")
Значение 3 по адресу 2017574962800
Умножение 3 на 7 на месте
Значение 21 по адресу 2017574962800
Умножать на месте можно за счет реализации метода __imul__
.
Все остальные операторы перегружаются аналогично.
Операторы сравнения#
Сравнение на |
Операторная форма |
Метод |
---|---|---|
Равенство |
|
|
Неравенство |
|
|
Меньше |
|
|
Меньше или равно |
|
|
Больше или равно |
|
|
Больше |
|
|
Все эти методы должны возвращать, True
, False
(или приводимое к ним значение), если сравнение произошло успешно, и NotImplemented
, если сравнение с объектом такого типа не предусмотренно. В отличие от арифметических операций, не существует отраженных версий операторов сравнения с префиксом r
. Вместо этого a.__gt__(b)
(a > b
) является отражением b.__lt__(a)
(b < a
) и т.п. Возвращенное значение NotImplemented
приведет к вызову отраженного оператора.
Метод __eq__
по умолчанию эквивалентен is
, т.е. сравнение двух экземпляров всегда даёт False
, если только это не один и тот же объект.
Tip
Если множество всех экземпляров класса является линейно упорядоченным, то можно воспользоваться декоратором total_ordering из модуля functools: необходимо определить лишь метод для ==
и один из операторов <
, <=
, >=
и >
, а остальные сгенерируются декоратором.
Множество целых чисел является линейно упорядоченным. Дополним пример с MyMutableInt
операторами сравнения <
и ==
, а остальные позволим сгенерировать декоратору total_ordering
.
from numbers import Number
from functools import total_ordering
@total_ordering
class MyMutableInt(Number):
def __init__(self, value):
if not isinstance(value, int):
raise TypeError("Допускаются только целые числа")
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return f"MyMutableInt(value={self.value})"
def __lt__(self, other):
if isinstance(other, int):
return self.value < other
if isinstance(other, MyMutableInt):
return self.value < other.value
return NotImplemented
def __eq__(self, other):
if isinstance(other, int):
return self.value == other
if isinstance(other, MyMutableInt):
return self.value == other.value
return NotImplemented
print(MyMutableInt(3) < MyMutableInt(7), MyMutableInt(3) <= 7)
print(MyMutableInt(42) > 13, MyMutableInt(42) >= 13)
print(42 == MyMutableInt(13))
True True
True True
False
Все операторы сравнения работают. В качестве бонуса мы теперь можем сортировать списки чисел типа MyMutableInt
.
from random import randint
L = [MyMutableInt(randint(-10, 10)) for _ in range(5)]
print(L)
print(sorted(L))
[MyMutableInt(value=6), MyMutableInt(value=-1), MyMutableInt(value=-4), MyMutableInt(value=5), MyMutableInt(value=10)]
[MyMutableInt(value=-4), MyMutableInt(value=-1), MyMutableInt(value=5), MyMutableInt(value=6), MyMutableInt(value=10)]
Побитовые операторы#
Операция |
Операторная форма |
Метод |
---|---|---|
Побитовое “и” |
|
|
Побитовое “или” |
|
|
Побитовое “исключающее или” |
|
|
Побитовое “не” |
|
|
Битовый сдвиг влево |
|
|
Битовый сдвиг вправо |
|
|
Note
Для всех бинарных побитовых операторов справедливы все те же замечания, что и для арифметических операторов, т.е. есть аналогичные методы с префиксами r
и i
для выполнения операций, когда объект стоит справа от оператора, и для выполнения операций на месте.
Операторы контейнеров#
Операция |
Операторная форма |
Метод |
---|---|---|
Проверка принадлежности |
|
|
Обращение по индексу |
|
|
Присваивание по индексу |
|
|
Удаление по индексу |
|
|
Метод __getitem__
мы неявно вызывали уже много раз. У списков и кортежей этот метод позволяет получить элемент по индексу, у строк получить символ. У словарей он позволяет получить значение по ключу. Также этот метод реализован у массивов NumPy
и у таблиц pandas
.
В качестве примера рассмотрим ещё одну возможную реализацию диапазона действительных чисел. Пусть конструктор принимает параметры start
, stop
и step
, а метод __getitem__
возвращает k
-й элемент последовательности, который вычисляется по формуле
где \(k\) в диапазоне от \(0\) до длинны последовательности \(\text{length}\), которая вычисляется по формуле
Вынесем вычисление \(k\)-го элемента последовательности в отдельный не публичный метод и предусмотрим индексацию отрицательными индексами.
from collections.abc import Sequence
from math import floor
class FloatRange(Sequence):
def __init__(self, start, stop, step=1.0):
self.start = start
self.stop = stop
if step == 0:
raise ValueError("Параметр step не может быть равен нулю")
self.step = step
self.length = floor((self.stop - self.start) / self.step) + 1
def __str__(self):
return f"range({self.start}, {self.stop}, {self.step})"
def __repr__(self):
return f"FloatRange(start={self.start}, stop={self.stop}, step={self.step})"
def __len__(self):
return self.length
def _kth_value(self, k):
return self.start + k * self.step
def __getitem__(self, k):
if isinstance(k, int):
if k < -self.length or k >= self.length:
raise IndexError("Индекс за пределами диапазона")
k = k + self.length if k < 0 else k
return self._kth_value(k)
raise TypeError("Индексация возможна только целым числом")
r = FloatRange(0., 3, 0.5)
for i in range(-r.length, r.length):
print(r[i], end=" ")
0.0 0.5 1.0 1.5 2.0 2.5 3.0 0.0 0.5 1.0 1.5 2.0 2.5 3.0
Класс collections.abc.Sequence — абстрактный базовый класс с абстрактными методами __len__
и __getitem__
, т.е. то, что мы наследовали от него, принуждает нас реализовать не только метод __getitem__
, но и метод __len__. Зато теперь для объектов этого класса автоматически генерируется ряд методов, такие как __contains__
и __iter__
. Такие сгенерированные методы называют примесями (mixin).
Это значит, что мы можем итерировать по объектам FloatRange
, не реализуя специальный метод __iter__
.
for x in FloatRange(3.0, 0.0, -0.5):
print(x, end=" ")
3.0 2.5 2.0 1.5 1.0 0.5 0.0
А так же проверять элемент на принадлежность диапазону.
r = FloatRange(0., 3, 0.5)
print(1.5 in r)
print(2.71 in r)
True
False
Но если попробовать получить срезу, то получиться ошибка, т.к. мы это не предусмотрели в методе __getitem__
.
try:
r[1:3]
except TypeError as msg:
print(msg)
Операторы последовательностей#
Операция |
Операторная форма |
Метод |
---|---|---|
Обращение по срезу |
|
|
Присваивание по срезу |
|
|
Удаление по срезу |
|
|
Чтобы можно было получать срез, необходимо предусмотреть ситуацию, когда в функцию __getitem__
передано не целое число, а объект slice.
Note
Выражение a[start:stop:step]
эквивалентно a[slice(start, stop, step)]
.
Расширим предыдущий пример функциональностью среза.
from collections.abc import Sequence
from math import floor
class FloatRange(Sequence):
def __init__(self, start, stop, step=1.0):
self.start = start
self.stop = stop
if step == 0:
raise ValueError("Параметр step не может быть равен нулю.")
self.step = step
self.length = floor((self.stop - self.start) / self.step) + 1
def __str__(self):
return f"range({self.start}, {self.stop}, {self.step})"
def __repr__(self):
return f"FloatRange(start={self.start}, stop={self.stop}, step={self.step})"
def __len__(self):
return self.length
def _kth_value(self, k):
return self.start + k * self.step
def __getitem__(self, k):
if isinstance(k, int):
if k < -self.length or k >= self.length:
raise IndexError("Индекс за пределами диапазона")
k = k + self.length if k < 0 else k
return self._kth_value(k)
if isinstance(k, slice):
slice_start = k.start if k.start else 0
slice_stop = k.stop
slice_step = k.step if k.step else 1
new_start = self._kth_value(slice_start)
new_stop = self._kth_value(slice_stop)
new_step = self.step * slice_step
return FloatRange(new_start, new_stop, new_step)
raise TypeError("Индексация возможна только целым числом или срезом")
r = FloatRange(0, 3, 0.5)
print(f"{r}[1:5:2]->{r[1:5:2]}")
print(f"{r}[5:1:-1]->{r[5:1:-1]}")
range(0, 3, 0.5)[1:5:2]->range(0.5, 2.5, 1.0)
range(0, 3, 0.5)[5:1:-1]->range(2.5, 0.5, -0.5)
Метод __getitem__
дополнился новым блоком кода.
if isinstance(k, slice):
slice_start = k.start if k.start else 0
slice_stop = k.stop
slice_step = k.step if k.step else 1
new_start = self._kth_value(slice_start)
new_stop = self._kth_value(slice_stop)
new_step = self.step * slice_step
return FloatRange(new_start, new_stop, new_step)
Параметры start
и step
среза по умолчанию приравниваются к None
. Первые три строки извлекают эти параметры и обрабатывают возможный None
. Следующие три строки вычисляют границы и шаг нового диапазона, который и возвращается в качестве результата.