Введение

В Python есть чрезвычайно полезный, но часто упускаемый из виду инструмент — dunder-методы. Это специальные методы, которые используются всеми классами и позволяют нам настраивать поведение определяемых пользователем классов. Если вы раньше программировали на Python, вы, вероятно, уже знакомы с наиболее распространенными методами dunder: __init__ и __str__.

Тем не менее, существуют десятки этих дандер-методов. Они позволяют нам настраивать поведение создаваемых нами классов в различных контекстах. В этом посте мы рассмотрим, как изменить поведение класса при использовании в арифметических операциях, таких как +, -, * и /. .

Я должен признаться — я не люблю числа. Итак, мы собираемся использовать методы дандера Python для создания нашей собственной системы счисления, называемой кватернионами.

Изучайте Python и приобретайте навыки, чтобы получить свою первую работу, с помощью нашего Учебного курса для профессиональных разработчиков Python.

Что такое кватернионы?

Кватернионы — это расширение комплексных чисел, имеющее следующую структуру:

Они находят применение во многих областях математики, физики и техники, робототехники и графики видеоигр, особенно потому, что они позволяют нам легко представлять повороты и ориентации в трехмерных пространствах. Чтобы узнать больше, вы можете посмотреть это видео с подробным объяснением, созданным отличным каналом YouTube 3blue1brown.

Кватернионы в Python

Для реализации кватернионов в Python мы создадим новый класс и переопределим, как эти числа складывают, вычитают, умножают и разделить. Начнем с создания метода __init__, где мы определим коэффициенты действительной и мнимой частей нашего кватерниона.

class Quaternion:

    def __init__(self, a, b, c, d):
        self.a = a
        self.b = b
        self.c = c
        self.d = d

Затем мы перезаписываем метод __str__, чтобы создать адекватное текстовое представление для этого типа числа:

class Quaternion:

    ...

    def __str__(self):
        return f"{self.a} + {self.b}i + {self.c}j + {self.d}k"

Теперь мы уже можем создать и увидеть наш первый кватернион:

q1 = Quaternion(1, 2, 3, 4)
print(q1)

# Result: 1 + 2i + 3j + 4k

Следующим шагом является адаптация этого класса для выполнения с ним основных арифметических операций. Начнем с определения того, как добавлять два кватерниона. Для этого мы можем перезаписать функцию __add__, которая принимает в качестве параметра другой кватернион. Сумма двух кватернионов рассчитывается как:

Итак, в Python мы можем реализовать эту операцию как:

class Quaternion:

    ...
    
    def __add__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for +: 'Quaternion' and '{type(other).__name__}'")
        return Quaternion(self.a + other.a, self.b + other.b, self.c + other.c, self.d + other.d)

С другой стороны, вычитание кватернионов определяется как:

Опять же, в Python мы можем реализовать это следующим образом:

class Quaternion:

    ...

    def __sub__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for -: 'Quaternion' and '{type(other).__name__}'")
        return Quaternion(self.a - other.a, self.b - other.b, self.c - other.c, self.d - other.d)

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

q1 = Quaternion(1, 2, 3, 4)
q2 = Quaternion(5, 6, 7, 8)

print(q1 + q2)
# Result: 6 + 8i + 10j + 12k

print(q1 - q2)
# Result: -4 + -4i + -4j + -4k

Следующим шагом является реализация произведения двух кватернионов, также известного как произведение Гамильтона. Эта операция немного сложнее, но ее можно записать так:

Каждая из строк определяет значение одного из коэффициентов: a, b, c и d соответственно. В Python мы можем реализовать это следующим образом, перезаписав функцию __mul__.

class Quaternion:

    ...    

    def __mul__(self, other):
        a = self.a * other.a - self.b * other.b - self.c * other.c - self.d * other.d
        b = self.a * other.b + self.b * other.a + self.c * other.d - self.d * other.c
        c = self.a * other.c - self.b * other.d + self.b * other.a + self.d * other.b
        d = self.a * other.d + self.b * other.c - self.c * other.b + self.d * other.a
        return Quaternion(a, b, c, d)

Наконец, мы собираемся определить разделение между кватернионами. Операция проста в выполнении, хотя и немного сложна для объяснения, поскольку она включает в себя понятия нормы и сопряжения, которые выходят за рамки этого руководства. Однако вы можете углубиться в эти понятия в статье Википедии о кватернионах. Подразделение определяется как:

В коде мы можем выполнить это следующим образом:

class Quaternion:

    ...

    def __truediv__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for /: 'Quaternion' and '{type(other).__name__}'")
        # Calculate the squared l2 norm of the other quaternion
        sq_norm = other.a ** 2 + other.b ** 2 + other.c ** 2 + other.d ** 2
        # Calculate the conjugate of the other quaternion
        conj = Quaternion(other.a, -other.b, -other.c, -other.d)
        # Calculate the product of the current quaternion and the conjugate of the other quaternion
        prod = self * conj
        # Divide the product by the squared l2 norm of the other quaternion
        return Quaternion(prod.a / sq_norm, prod.b / sq_norm, prod.c / sq_norm, prod.d / sq_norm)

Если мы сейчас протестируем эти две операции, то увидим, что они работают правильно:

q1 = Quaternion(1, 2, 3, 4)
q2 = Quaternion(5, 6, 7, 8)

print(q1 / q2)
# Result: 0.40229885057471265 + 0.04597701149425287i + -0.028735632183908046j + 0.09195402298850575k

Рассмотренные до сих пор методы позволяют нам выполнять основные арифметические операции. Однако существует версия каждого из этих операторов, которая помимо выполнения операции присваивает результат целевой переменной. Конечно, я имею в виду операторы: +=, -=, *= и /= . В Python также есть специальные методы для перезаписи этих операторов. Они имеют то же имя, что и предыдущие, с предшествующей буквой «i»: __iadd__, __isub__, __imul__ и __itruediv__. После того, как мы реализовали предыдущие методы, их легко реализовать:

class Quaternion:

    ...

    def __iadd__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for +=: 'Quaternion' and '{type(other).__name__}'")
        self.a += other.a
        self.b += other.b
        self.c += other.c
        self.d += other.d
        return self
    
    def __isub__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for -=: 'Quaternion' and '{type(other).__name__}'")
        self.a -= other.a
        self.b -= other.b
        self.c -= other.c
        self.d -= other.d
        return self
    
    def __imul__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for *=: 'Quaternion' and '{type(other).__name__}'")
        self.a = self.a * other.a - self.b * other.b - self.c * other.c - self.d * other.d
        self.b = self.a * other.b + self.b * other.a + self.c * other.d - self.d * other.c
        self.c = self.a * other.c - self.b * other.d + self.b * other.a + self.d * other.b
        self.d = self.a * other.d + self.b * other.c - self.c * other.b + self.d * other.a
        return self
    
    def __itruediv__(self, other):
        if not isinstance(other, Quaternion):
            raise TypeError(f"unsupported operand type(s) for /=: 'Quaternion' and '{type(other).__name__}'")
        sq_norm = other.a ** 2 + other.b ** 2 + other.c ** 2 + other.d ** 2
        conj = Quaternion(other.a, -other.b, -other.c, -other.d)
        prod = self * conj
        self.a = prod.a / sq_norm
        self.b = prod.b / sq_norm
        self.c = prod.c / sq_norm
        self.d = prod.d / sq_norm
        return self

Подведение итогов

Короче говоря, методы Python dunder позволяют нам изменять то, как наши объекты реагируют на различные основные операции.

В следующем посте этой серии мы продолжим рассмотрение примера с кватернионами, чтобы узнать о других методах Дандера, которые позволят нам сравнить величину наших чисел. Увидимся там!

Эту и многие другие истории читайте на сайте:

https://evlabs.io