Магия в Python — создаём объект с магическими методами (ч. 1)

Добрый день, уважаемые читатели. 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-канал. Там еще больше полезного и интересного для программистов.