Добрый день, уважаемые читатели. Python всегда славился своим удобством по отношению к пользователю, это не для кого не секрет.
Лично мне всегда нравились «магические» методы — если по простому, то это методы, которые вызываются неявно во время вызова функций или выполнения различных операций.
В этой статье мы создадим класса n-мерного вектора, который будет использовать магические методы, которые согласуются с логикой операций над векторами.
Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.
А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!
Инициализация
Инициализация экземпляра класса осуществляется с помощью передачи аргументов в скобки, а обрабатывает эти параметры магический метод __init__()
. Объявим класс Vector
и определим там этот метод:
class Vector(object):
def init(self, *args):
if isinstance(args[0], int) or isinstance(args[0], float):
self.__values = tuple(args)
else:
if len(args) != 1:
raise ValueError('...')
self.__values = tuple(args[0])
self.shape = len(self.__values)
В описании ValueError
указано троеточие лишь из-за того, что полное описание ошибки не влезет в блок кода. В конце статьи будет оставлена ссылка на исходник, где будет присутствовать полное описание ошибок.
Такое объявление конструктора класса даёт нам возможность создать экземпляр класса двумя способами:
v = Vector(1, 2, 3)
v = Vector([1, 2, 3])
Поле shape
создано для имитации интерфейса NumPy
, где это поле присутствует у каждого экземпляра класса numpy.ndarray
.
Использование встроенных функций
Теперь реализуем несколько магических методов, которые используют встроенные функции.
Первый из них __len__()
, который использует функция len
— вызов len(obj)
эквивалентно конструкции obj.__len__()
.
def __len__(self):
return self.shape
Протестируем эту возможность:
>>> v.shape, len(v) (3, 3)
Далее мы будем вычислять модуль вектора, то есть его длину. Формулу длины вектора приводить не буду, её и так все знают. Функция abs()
(название от англ. absolute) использует магический метод __abs__()
. Объявим этот метод в нашем классе:
def __abs__(self):
return sum(self**2)**0.5
>>> abs(v) 3.7416573867739413
Функция sum()
в качестве аргумента принимает итерируемый объект. Реализация итерации по объекту возможна с помощью метода __iter__()
.
def __iter__(self):
return iter(self.__values)
Пример использования:
for el in v: print(el) 1 2 3
Теперь реализуем методы конвертации в строку и метод, отвечающий за представление объекта в интерпретаторе.
Первый из них — __str__()
, второй — __repr__ ()
.
def __str__(self) -> str:
return '<' + ', '.join([str(v) for v in self]) + '>'
def __repr__(self) -> str:
return 'Vector(' + self.__str__() + ')'
Чтобы протестировать эти методы, воспользуемся функцией print()
и интерпретатором Python.
>>> print(v) <1, 2, 3> >>> v Vector(<1, 2, 3>)
Математические магические методы
Теперь мы займемся перегрузкой операторов — сделаем так, чтобы операторы +, -, /, * и т.д. могли использовать наш класс в качестве операнда.
Наша идея заключается в следующем — если мы складываем (делим и другие различные операции) вектор с числом — мы прибавляем число к каждому элементу вектора и возвращаем новый экземпляр класса Vector
, т. е. применяем операцию векторизированно. Если же вторым операндом является другой вектор — проверяем, имеют ли операнды равную длину и выполняем операцию поэлементно.
Концепция перегрузки операторов одинакова, потому я предоставляю список различных методов и соответствующих операций:
__add__()
—v + 1
__sub__()
—v - 1
__mul__()
—v * 1
__div__()
—v / 1
__mod__()
—v % 1
__floordiv__()
—v / / 1
__divmod__()
—divmod(v, 1)
__pow__()
—v ** 1
__lshift__()
—v << 1
__rshift__()
—v >> 1
Это далеко не все магические методы, связанные с математическими операциями, однако этого нам на данный вполне достаточно. Ниже вы можете видеть реализацию всех этих методов:
def __mul__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value * coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 * v1 for v1, v2 in zip(value, self)]) else: raise TypeError(f'...') def __div__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor / value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 / v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __add__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor + value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(...') return Vector([v1 + v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __sub__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor - value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'Wrong dimension for operand. Expected {self.shape}, got {value.shape}') return Vector([v1 - v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __mod__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor % value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'Wrong dimension for operand. Expected {self.shape}, got {value.shape}') return Vector([v1 % v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __floordiv__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor // value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 // v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __divmod__(self, value): raise TypeError('This class does not support using with divmod() function.') def __pow__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor ** value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 ** v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __lshift__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor << value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 << v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rshift__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([coor >> value for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v1 >> v2 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...')
Наш объект не поддерживает метод __divmod__()
, т.к. использование функции divmod()
не совсем соответствует нашей идеи о возвращении нового экземпляра Vector
.
Обратные операции
Также существуют магические методы, отвечающие за обратные операции, к примеру, выражение 1 + v
вызывает v.__radd__(1)
.
Все эти методы объявляются также, как и простые математические методы, только с буквой r в начале по аналогии c __radd__()
.
Реализуем все эти методы в нашем классе:
def __rsub__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value - coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 - v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rpow__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value ** coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 ** v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rdiv__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value / coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 / v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rfloordiv__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value // coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 // v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rmod__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value % coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 % v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __rlshift__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value << coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 << v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...') def __radd__(self, value): return self.__add__(value) def __rmul__(self, value): return self.__mul__(value) def __rrshift__(self, value): if isinstance(value, int) or isinstance(value, float): return Vector([value >> coor for coor in self]) elif isinstance(value, Vector): if value.shape != self.shape: raise ValueError(f'...') return Vector([v2 >> v1 for v1, v2 in zip(self, value)]) else: raise TypeError(f'...')
Теперь протестируем выполнение математических операций с нашим вектором <1, 2, 3>
.
>>> v * 2, 3 * v, v * Vector([1, 2, 3]) (Vector(<2, 4, 6>), Vector(<3, 6, 9>), Vector(<1, 4, 9>))
>>> v + 2, v - 1, 1 - v, 1 + v (Vector(<3, 4, 5>), Vector(<0, 1, 2>), Vector(<0, -1, -2>), Vector(<2, 3, 4>))
>>> v + Vector([1, 2, 3]), v - Vector([0, 1, 0]) (Vector(<2, 4, 6>), Vector(<1, 1, 3>))
>>> v ** 2, 2 ** v, v ** Vector([1, 2, 3]) (Vector(<1, 4, 9>), Vector(<2, 4, 8>), Vector(<1, 4, 27>))
>>> v << 1, v >> 2, 2 >> v, 1 << v (Vector(<2, 4, 6>), Vector(<0, 0, 0>), Vector(<1, 0, 0>), Vector(<2, 4, 8>))
Если тестировать все операции, то статья выйдет слишком большой, но главную суть вы уловили.
Магические методы какЛогические операции
В Python есть три логические операции — И, ИЛИ, XOR.
Соответствующие им магические методы: __and__()
, __or__()
, __xor__()
. Для них также существуют обратные операции — __rand__()
, __ror__(
) и __rxor__()
. Реализация этих методов в нашем классе выглядит следующим образом:
def __and__(self, value): return bool(value) & bool(self) def __or__(self, value): return bool(value) | bool(self) def __xor__(self, value): return bool(value) ^ bool(self) def __and__(self, value): return bool(value) & bool(self) def __or__(self, value): return bool(value) | bool(self) def __xor__(self, value): return bool(value) ^ bool(self)
Здесь есть один очень важный момент — bool(self)
. Т.е. мы вызываем функцию bool
, передав ей текущий экземпляр класса.
Функция bool
использует магический метод __bool__()
, который возвращает булевое значение, как ни странно.
Наша логика заключается в следующем — если хотя бы один элемент вектора не равен 0, то булевое значение вектора равно True
, и False
в обратном случае.
def __bool__(self) -> bool:
for value in self.__values:
if value != 0:
return True
return False
Протестируем логические операции с нашим вектором:
>>> v & 1, v & 0, v | 1, v ^ 1 (True, False, True, False)
>>> 1 & v, 0 | 1, 0 ^ v (True, 1, True)
Заключение
Сегодня мы разобрали множество магических методов, однако это далеко не всё. В ближайшее время выйдет продолжение этой статьи, где будут рассмотрены другие методы, а также альтернативные конструкторы. Также мы добавим функционал, свойственный вектору — к примеру, скалярное произведение с другим вектором.
Ссылка на код класса из статьи вы можете найти здесь.
На этом статья заканчивается. Если вы заинтересованы в математическом анализе, советую прочитать «Производная. Базовые определения и термины«.
А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.