[PYTHON] The story of making an immutable mold

Introduction

Self-introduction

This is H1rono.

What to do in this article

I made an immutable (immutable) type in pure python. I hope I can tell you in detail what I did.

What do you make

Make a vector type. Specifically,

Create a class with such requirements.

Step 1-Immutable

First, implement the "immutable" requirement.

What is immutable?

Immutable quote from Official Glossary

immutable (Immutable) An object with a fixed value. Immutable objects include numbers, strings, and tuples. The values of these objects cannot be changed. If you want to remember another value, you have to create a new object. Immutable objects play an important role in situations where fixed hash values are required. A dictionary key is an example.

In other words, it seems that you cannot say vector.direction = 256.

Implementation

What's great is the named tuple in the built-in collections module. With the 3 / library / collections.html # collections.namedtuple) function, you can easily create a subclass of tuple (that is, immutable) with a name assigned to that element. python is amazing.

vec.py


from collections import namedtuple


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    pass #Meet other requirements here


if __name__ == '__main__':
    vec = MyVector(4, 2)
    print(vec) #MyVector(length=4, direction=2)

Execution result:

MyVector(length=4, direction=2)

Now, among the requirements mentioned above,

Was able to be implemented for the time being. The remaining requirements are

is. It has decreased all at once.

Step 2-Create Property

The following implements the requirement that "coordinates as a position vector can be referenced by property named x, y ". Use the built-in math module. The expression for x islength * cos (pi * direction / 180), and the expression for y is length * sin (pi * direction / 180).

vec.py


from collections import namedtuple
from math import pi, cos, sin #Add import


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    #Additional part from here#
    @property
    def x(self):
        theta = pi * self.direction / 180
        return self.length * cos(theta)

    @property
    def y(self):
        theta = pi * self.direction / 180
        return self.length * sin(theta)

    #Additional part so far#


if __name__ == '__main__':
    from math import sqrt
    root2 = sqrt(2)
    vec = MyVector(root2, 45)
    print('x = {}'.format(vec.x)) #x = 1
    print('y = {}'.format(vec.y)) #y = 1

As you can see by executing the above code, the execution result is not as commented. In my environment,

x = 1.0000000000000002
y = 1.0

It will be displayed. This behavior is probably related to here (floating point arithmetic, its problems and limitations). think. If you are interested, please. Anyway, for the time being

Was implemented. The rest,

is. It's an implementation of all operations.

Step 3-Implementation of arithmetic

how?

Reference: Official Reference (emulates numeric types)

The correspondence between the operators and method names implemented this time is roughly as shown in the following table.

operator Method name
+ __add__, __radd__
- __sub__, __rsub__
* __mul__, __rmul__
/ __truediv__, __rtruediv__

All functions have (self, other) arguments. In other words, when vector = Myvector (8, 128), vector + x callsvector .__ add__ (x)and returns the result as a calculation result. Methods with r at the beginning, such as __radd__, implement "the operands are reflected (replaced)" (quoted from the URL above). That is, as x + vector, whenx.__ add__ (vector)returns Not Implemented, the result of vector.__radd__ (x) is returned. Since it's a big deal, I'll add this one as well. The implementation method is like this. Let's write it. I would like to say that, in the implementation of addition and subtraction, it is necessary to obtain the vector represented by (length, direction) from the coordinates of (x, y). First, implement that method.

Implementation (direction from coordinates, size)

vec.py


from collections import namedtuple
from math import pi, cos, sin, sqrt, acos #Add import


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    #Additional part from here#
    @classmethod
    def from_coordinates(cls, p1, p2=None):
        #p1: (x, y)
        #p2: (x, y)Or None
        if p2 is None:
            #Treat as a position vector
            x, y = p1
        else:
            #Starting from p1,Treat p2 as an endpoint vector
            x = p2[0] - p1[0]
            y = p2[1] - p1[1]
        r = sqrt(x**2 + y**2)
        if r == 0:
            #0 vector
            return cls(0, 0)
        #Inverse trigonometric function
        theta = 180 * acos(x / r) / pi
        if y < 0:
            # 180 < direction < 360
            #theta=32 -> direction=328
            #theta=128 -> direction=232
            theta = 360 - theta
        return cls(r, theta)

    #Additional part so far#

    @property
    def x(self):
        theta = pi * self.direction / 180
        return self.length * cos(theta)

    @property
    def y(self):
        theta = pi * self.direction / 180
        return self.length * sin(theta)


if __name__ == '__main__':
    vec = MyVector.from_coordinates((2, 2))
    print('x = {}'.format(vec.x)) #x = 2
    print('y = {}'.format(vec.y)) #y = 2

Added the from_coordinates function. As the name implies, it creates a MyVector object from the coordinates. I chose the class method because I found it very convenient. Execution result:

x = 1.9999999999999996
y = 2.0000000000000004

See you later ... You can add the round function and round it later. Anyway, I was able to get the vector represented by (length, direction) from the coordinates of (x, y).

Implementation (addition, subtraction)

Next, we will implement addition and subtraction. Since the supported calculation target is a vector of the same type, the condition for determining that it is a vector is "the length is 2, and both the first and second elements are real numbers". This determination will be used again later, so make it a function.

vec.py


from collections import namedtuple
from math import pi, cos, sin, sqrt, acos
from numbers import Real #Add import


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    #Additional part from here#
    def __add__(self, other):
        #Implementation of addition
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x + v.x, self.y + v.y))
        else:
            return NotImplemented

    #The so-called reflective version of addition
    __radd__ = __add__

    def __sub__(self, other):
        #Implementation of subtraction
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x - v.x, self.y - v.y))
        else:
            return NotImplemented

    def __rsub__(self, other):
        #Reflective version of subtraction
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((v.x - self.x, v.y - self.y))
        else:
            return NotImplemented

    @staticmethod
    def isvector(obj):
        #Function to determine if it is a vector
        try:
            return (
                isinstance(obj[1], Real)
                and isinstance(obj[0], Real)
                and len(obj) == 2)
        except:
            return False

    #Additional part so far#

    @classmethod
    def from_coordinates(cls, p1, p2=None):
        if p2 is None:
            x, y = p1
        else:
            x = p2[0] - p1[0]
            y = p2[1] - p1[1]
        r = sqrt(x**2 + y**2)
        if r == 0:
            return cls(0, 0)
        theta = 180 * acos(x / r) / pi
        if y < 0:
            theta = 360 - theta
        return cls(r, theta)

    @property
    def x(self):
        theta = pi * self.direction / 180
        return self.length * cos(theta)

    @property
    def y(self):
        theta = pi * self.direction / 180
        return self.length * sin(theta)


if __name__ == '__main__':
    vec1 = MyVector.from_coordinates((1, 1))
    vec2 = MyVector.from_coordinates((-1, -1))
    print(vec1 + vec2) #MyVector(length=0, direction=0)
    print(vec1 - vec2) #MyVector(length=2.8284...[2√2], direction=45)

The Real class in the built-in numbers module is a real base class. ʻIsinstance (1024, Real), ʻisinstance (1.024, Real), etc. all return True. We set __radd__ = __add__ because vector + x and x + vector are equivalent. When translated like javascript, it looks like this.__radd__ = this .__ add__. Execution result:

MyVector(length=4.965068306494546e-16, direction=153.43494882292202)
MyVector(length=2.8284271247461903, direction=45.00000000000001)

I don't know if it fits. Round it with the round function to make it easier to see.

Implementation (round)

vec.py


from collections import namedtuple
from math import pi, cos, sin, sqrt, acos
from numbers import Real


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    def __add__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x + v.x, self.y + v.y))
        else:
            return NotImplemented

    __radd__ = __add__

    def __sub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x - v.x, self.y - v.y))
        else:
            return NotImplemented

    def __rsub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((v.x - self.x, v.y - self.y))
        else:
            return NotImplemented

    @classmethod
    def from_coordinates(cls, p1, p2=None):
        if p2 is None:
            x, y = p1
        else:
            x = p2[0] - p1[0]
            y = p2[1] - p1[1]
        #here
        r = round(sqrt(x  ** 2 + y ** 2), 3)
        if r == 0:
            return cls(0, 0)
        #here
        theta = round(180 * acos(x / r) / pi)
        if y < 0:
            theta = 360 - theta
        return cls(r, theta)

    @property
    def x(self):
        theta = pi * self.direction / 180
        #here
        return round(self.length * cos(theta), 3)

    @property
    def y(self):
        theta = pi * self.direction / 180
        #here
        return round(self.length * sin(theta), 3)


if __name__ == '__main__':
    vec1 = MyVector.from_coordinates((1, 1))
    vec2 = MyVector.from_coordinates((-1, -1))
    print(vec1 + vec2) #MyVector(length=0, direction=0)
    print(vec1 - vec2) #MyVector(length=2.828, direction=45)

Execution result:

MyVector(length=0, direction=0)
MyVector(length=2.828, direction=45)

it is a good feeling. By the way, the direction of the result of addition is changed from153.43 ...to 0, because it was caught in ʻif r == 0:infrom_coordinates`. As expected. For the time being

Was implemented. The remaining requirements are

is. Next we will implement multiplication.

Implementation (multiplication)

Multiplication is a requirement that the behavior changes depending on the calculation target. For real numbers and for vectors. You can use the Real class used earlier to determine the real number. The ʻis vector` function is useful for determining vectors.

vec.py


from collections import namedtuple
from math import pi, cos, sin, sqrt, acos
from numbers import Real


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    def __add__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x + v.x, self.y + v.y))
        else:
            return NotImplemented

    __radd__ = __add__

    def __sub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x - v.x, self.y - v.y))
        else:
            return NotImplemented

    def __rsub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((v.x - self.x, v.y - self.y))
        else:
            return NotImplemented

    #Additional part from here#
    def __mul__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            #For real numbers
            l = round(self.length * other, 3)
            return cls(l, self.direction)
        elif cls.isvector(other):
            #For vector
            theta = pi * (other[1] - self[1]) / 180
            product = self[0] * other[0] * cos(theta)
            return round(product, 3)
        else:
            #Otherwise
            return NotImplemented

    __rmul__ = __mul__

    #Additional part so far#

    @staticmethod
    def isvector(obj):
        try:
            return (
                isinstance(obj[1], Real)
                and isinstance(obj[0], Real)
                and len(obj) == 2)
        except:
            return False

    @classmethod
    def from_coordinates(cls, p1, p2=None):
        if p2 is None:
            x, y = p1
        else:
            x = p2[0] - p1[0]
            y = p2[1] - p1[1]
        r = round(sqrt(x**2 + y**2), 3)
        if r == 0:
            return cls(0, 0)
        theta = round(180 * acos(x / r) / pi)
        if y < 0:
            theta = 360 - theta
        return cls(r, theta)

    @property
    def x(self):
        theta = pi * self.direction / 180
        return round(self.length * cos(theta), 3)

    @property
    def y(self):
        theta = pi * self.direction / 180
        return round(self.length * sin(theta), 3)


if __name__ == '__main__':
    vec1 = MyVector(2, 45)
    vec2 = MyVector(2, 135)
    print(vec1 * 2) #MyVector(length=4, direction=45)
    print(vec1 * vec2) #0

Execution result:

MyVector(length=4, direction=45)
0.0

with this,

Was implemented. The remaining requirements are

is. One more!

Implementation (division)

We will implement it in the same way as multiplication. As I wrote, I noticed that multiplication and division of real numbers and vectors is a vector with vector1 / vector2 = vector1 * (1 / vector2) if you think that the size of the real number and the vector are multiplied and divided. It seems that division between each other can also be implemented. I will add that as well.

vec.py


from collections import namedtuple
from math import pi, cos, sin, sqrt, acos
from numbers import Real


_MyVector = namedtuple('_MyVector', ('length', 'direction'))
class MyVector(_MyVector):
    def __add__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x + v.x, self.y + v.y))
        else:
            return NotImplemented

    __radd__ = __add__

    def __sub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((self.x - v.x, self.y - v.y))
        else:
            return NotImplemented

    def __rsub__(self, other):
        if self.isvector(other):
            cls = self.__class__
            v = cls(*other)
            return cls.from_coordinates((v.x - self.x, v.y - self.y))
        else:
            return NotImplemented

    def __mul__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            l = round(self.length * other, 3)
            return cls(l, self.direction)
        elif cls.isvector(other):
            theta = pi * (other[1] - self[1]) / 180
            product = self[0] * other[0] * cos(theta)
            return round(product, 3)
        else:
            return NotImplemented

    __rmul__ = __mul__

    #Additional part from here#
    def __truediv__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            l = round(self.length / other, 3)
            return cls(l, self.direction)
        elif cls.isvector(other):
            return self * (1 / cls(*other))
        else:
            return NotImplemented

    #Reflective version of division
    def __rtruediv__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            l = round(other / self.length, 3)
            return cls(l, self.direction)
        elif cls.isvector(other):
            return other * (1 / self)
        else:
            return NotImplemented

    #Additional part so far#

    @staticmethod
    def isvector(obj):
        try:
            return (
                isinstance(obj[1], Real)
                and isinstance(obj[0], Real)
                and len(obj) == 2)
        except:
            return False

    @classmethod
    def from_coordinates(cls, p1, p2=None):
        if p2 is None:
            x, y = p1
        else:
            x = p2[0] - p1[0]
            y = p2[1] - p1[1]
        r = round(sqrt(x**2 + y**2), 3)
        if r == 0:
            return cls(0, 0)
        theta = round(180 * acos(x / r) / pi)
        if y < 0:
            theta = 360 - theta
        return cls(r, theta)

    @property
    def x(self):
        theta = pi * self.direction / 180
        return round(self.length * cos(theta), 3)

    @property
    def y(self):
        theta = pi * self.direction / 180
        return round(self.length * sin(theta), 3)


if __name__ == '__main__':
    vec1 = MyVector(2, 45)
    print(vec1 / 2) #MyVector(length=1, direction=45)
    print(vec1 / vec1) #1

Execution result:

MyVector(length=1.0, direction=45)
1.0

It went well. Now all the requirements are implemented!

Step 4-Try it

>>> vec1 = MyVector(2, 45)
>>> vec2 = MyVector(2, 135)
>>> vec3 = MyVector(2, 225)
>>> vec1.direction = 256
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: can`t set attribute
>>> vec1 + vec3
MyVector(length=0, direction=0)
>>> vec1 - vec3
MyVector(length=3.999, direction=45)
>>> vec1 * vec2
0.0
>>> vec1 * (2, 135)
0.0
>>> vec1 * 2
MyVector(length=4, direction=45)
>>> vec1 / 2
MyVector(length=1.0, direction=45)
>>> vec1 += vec3
>>> vec1
MyVector(length=0, direction=0)

Only the result of vec1 --vec3 is a little strange, but roughly as intended.

At the end

I created an immutable vector type in pure python. It's not strange here, if you don't understand here, please tell me anything. The value deviation was noticeable on the way, but if you are interested, try using here (decimal built-in module) etc. How is it?

bonus

`(x, y)` vector type

vec2.py


from math import pi, sqrt, acos
from numbers import Real
from operator import itemgetter


class MyVector2(tuple):
    _ROUND_DIGIT = 2

    __slots__ = ()

    def __new__(cls, x=0, y=0):
        return tuple.__new__(cls, (x, y))

    def __repr__(self):
        t = (self.__class__.__name__, *self)
        return ('{0}(x={1}, y={2})'.format(*t))

    __bool__ = (lambda self: bool(self.length))

    def __add__(self, other):
        cls = self.__class__
        if cls.isvector(other):
            return cls(self[0] + other[0], self[1] + other[1])
        else:
            return NotImplemented

    def __sub__(self, other):
        cls = self.__class__
        if cls.isvector(other):
            return cls(self[0] - other[0], self[1] - other[1])
        else:
            return NotImplemented

    def __mul__(self, other):
        cls = self.__class__
        if cls.isvector(other):
            return self[0]*other[0] + self[1]*other[1]
        elif isinstance(other, Real):
            return cls(self.x * other, self.y * other)
        else:
            return NotImplemented

    def __truediv__(self, other):
        cls = self.__class__
        if cls.isvector(other):
            v = cls(*other)
            return self * (1 / v)
        elif isinstance(other, Real):
            return cls(self.x / other, self.y / other)
        else:
            return NotImplemented

    __radd__ = __add__
    __rmul__ = __mul__

    def __rsub__(self, other):
        return -(self - other)

    def __rtruediv__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            return cls(other / self.x, other / self.y)
        elif cls.isvector(other):
            return other * (1 / self)
        else:
            return NotImplemented

    __abs__ = (lambda self: abs(self.length))
    __pos__ = (lambda self: self)

    def __neg__(self):
        x, y = -self.x, -self.y
        return self.__class__(x, y)

    def __complex__(self):
        return self.x + self.y * 1j

    @staticmethod
    def isvector(obj):
        try:
            return (
                isinstance(obj[1], Real)
                and isinstance(obj[0], Real)
                and len(obj) == 2)
        except:
            return False

    @classmethod
    def _round(cls, a):
        return round(a, cls._ROUND_DIGIT)

    x = property(itemgetter(0))
    y = property(itemgetter(1))

    @property
    def length(self):
        r = sqrt(self.x**2 + self.y**2)
        return self._round(r)

    @property
    def direction(self):
        r = self.length
        if r == 0:
            return 0
        x, y = self
        theta = self._round(180 * acos(x / r) / pi)
        if y < 0:
            theta = 360 - theta
        return theta

I will try using it.

>>> v1 = MyVector2(1, 1)
>>> v2 = MyVector2(-1, -1)
>>> v1 + v2
MyVector2(x=0, y=0)
>>> v1 - v2
MyVector2(x=2, y=2)
>>> v1 * v2
-2
>>> v1 / v2
-2.0
>>> v1.direction
44.83
>>> v1.length
1.41
>>> v2.direction
224.83
>>> v1 / (-1, -1)
-2.0
>>> v1 + (1, 1)
MyVector2(x=2, y=2)
>>> (1, 1) + v1
MyVector2(x=2, y=2)
>>> v1 - (1, 1)
MyVector2(x=0, y=0)
>>> (1, 1) - v1
MyVector2(x=0, y=0)
>>> v1 * 2
MyVector2(x=2, y=2)
>>> v1 / 2
MyVector2(x=0.5, y=0.5)
Introduce ABC to make the `(length, direction)` vector and the `(x, y)` vector compatible

vector.py


from operator import itemgetter
from math import pi, cos, sin, sqrt, acos
from numbers import Real
import abc


def isvector(obj):
    try:
        return (
            isinstance(obj[0], Real)
            and isinstance(obj[1], Real)
            and	len(obj) == 2)
    except:
        return False


def empty(*args, **kwargs): pass


class MyABCMeta(abc.ABCMeta):
    def __new__(cls, name, bases=(), namespace={}, ab_attrs=(), **kwargs):
        namespace.update(cls.__prepare__(name, bases, ab_attrs))
        return super().__new__(cls, name, bases, namespace, **kwargs)

    @classmethod
    def __prepare__(cls, name, bases, ab_attrs=()):
        result = {
            name: abc.abstractmethod(empty) \
                for name in ab_attrs
        }
        return result


class ABVector(
        tuple,
        metaclass=MyABCMeta,
        ab_attrs=(
            'x', 'y', 'length', 'direction', #properties
            'from_xy', 'from_ld', #converter [argments: (cls, vector: tuple)]
            '__repr__', '__bool__', '__abs__',
            '__pos__', '__neg__', '__complex__',
            '__eq__', '__add__', '__radd__',
            '__sub__', '__rsub__','__mul__',
            '__rmul__', '__truediv__', '__rtruediv__'
        )
    ):
    __slots__ = ()

    ROUND_DIGIT = 3

    @classmethod
    def round(cls, a):
        return round(a, cls.ROUND_DIGIT)

    @classmethod
    def __instancecheck__(cls, instance):
        return all(map(
            (lambda string: hasattr(instance, string)),
            ('x', 'y', 'length', 'direction')
        ))

    @classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)


class XYVector(ABVector):
    _fields_ = ('x', 'y')
    
    def __new__(cls, x=0, y=0):
        t = (cls.round(x), cls.round(y))
        return super().__new__(cls, t)

    def __repr__(self):
        t = (self.__class__.__name__, *self)
        return f'{self.__class__.__name__}(x={self.x}, y={self.y})'

    def __bool__(self):
        return bool(self.length)

    def __abs__(self):
        return abs(self.length)

    __pos__ = lambda self: self

    def __neg__(self):
        t = (-self.x, -self.y)
        return self.__class__(*t)

    def __complex__(self):
        return self.x + self.y * 1j

    def __eq__(self, other):
        try:
            return (
                self.x == other.x
                and self.y == other.y
            )
        except:
            return False

    # operation implementation: start #

    template = """
    def __{name}__(self, other):
        cls = self.__class__
        if isinstance(other, ABVector):
            x = {0} self.x {1} other.x
            y = {0} self.y {1} other.y
            return cls(x, y)
        elif isvector(other):
            vec = cls(*other)
            x = {0} self.x {1} vec.x
            y = {0} self.y {1} vec.y
            return cls(x, y)
        else:
            return NotImplemented
    """.replace(f'\n{" " * 4}', '\n')

    order = (
        (('+', '+'), {'name': 'add'}),
        (('+', '+'), {'name': 'radd'}),
        (('+', '-'), {'name': 'sub'}),
        (('-', '+'), {'name': 'rsub'}),
    )
    for t, d in order:
        exec(template.format(*t, **d))

    template = """
    def __{name}__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            x = cls.round({0} self.x {1} other)
            y = cls.round({0} self.y {1} other)
            return cls(x, y)
        elif isinstance(other, ABVector):
            product = (self.x * other.x + self.y * other.y) / {2}
            return cls.round(product)
        elif isvector(other):
            other = cls(*other)
            product = (self.x * other.x + self.y * other.y) / {2}
            return cls.round(product)
        else:
            return NotImplemented
    """.replace(f'\n{" " * 4}', '\n')

    order = (
        (('', '*', 1), {'name': 'mul'}),
        (('', '*', 1), {'name': 'rmul'}),
        (('', '/', '(other.x ** 2 + other.y ** 2)'), {'name': 'truediv'}),
        (('1 /', '*', '(self.x ** 2 + self.y ** 2)'), {'name': 'rtruediv'}),
    )
    for t, d in order:
        exec(template.format(*t, **d))

    del template, order, t, d

    # operation implementation: end #

    @classmethod
    def from_xy(cls, vector):
        return cls(*vector)

    @classmethod
    def from_ld(cls, vector):
        l, d = vector
        t = pi * d / 180
        x = l * cos(t)
        y = l * sin(t)
        return cls(x, y)

    x = property(itemgetter(0))
    y = property(itemgetter(1))

    @property
    def length(self):
        r = sqrt(self.x ** 2 + self.y ** 2)
        return self.__class__.round(r)

    @property
    def direction(self):
        r = self.length
        if r == 0:
            return 0
        x, y = self
        theta = round(180 * acos(x / r) / pi)
        if y < 0:
            theta = 360 - theta
        return theta


class LDVector(ABVector):
    _fields_ = ('length', 'direction')

    def __new__(cls, length=0, direction=0):
        t = (cls.round(length), round(direction))
        return super().__new__(cls, t)

    def __repr__(self):
        return f'{self.__class__.__name__}(length={self.length}, direction={self.direction})'

    def __bool__(self):
        return bool(self.length)

    def __abs__(self):
        return abs(self.length)

    __pos__ = lambda self: self

    def __neg__(self):
        d = self.direction + 180
        if d <= 360:
            d -= 360
        return self.__class__(self.length, d)

    def __complex__(self):
        return self.x + self.y * 1j

    def __eq__(self, other):
        try:
            return (
                self.length == other.length
                and self.direction == other.direction
            )
        except:
            return False

    # operation implementation: start #

    template = """
    def __{name}__(self, other):
        cls = self.__class__
        if isinstance(other, ABVector):
            x = {0} self.x {1} other.x
            y = {0} self.y {1} other.y
            return cls.from_xy((x, y))
        elif isvector(other):
            vec = cls(*other)
            x = {0} self.x {1} vec.x
            y = {0} self.y {1} vec.y
            return cls.from_xy((x, y))
        else:
            return NotImplemented
    """.replace(f'\n{" " * 4}', '\n')

    order = (
        (('+', '+'), {'name': 'add'}),
        (('+', '+'), {'name': 'radd'}),
        (('+', '-'), {'name': 'sub'}),
        (('-', '+'), {'name': 'rsub'}),
    )
    for t, d in order:
        exec(template.format(*t, **d))

    template = """
    def __{name}__(self, other):
        cls = self.__class__
        if isinstance(other, Real):
            l = cls.round({0} self.length {1} other)
            return cls(l, self.direction)
        elif isinstance(other, ABVector):
            t = pi * (self.direction - other.direction) / 180
            product = (self.length * other.length * cos(t)) / {2}
            return cls.round(product)
        elif isvector(other):
            other = cls(*other)
            t = pi * (self.direction - other.direction) / 180
            product = (self.length * other.length * cos(t)) / {2}
            return cls.round(product)
        else:
            return NotImplemented
    """.replace(f'\n{" " * 4}', '\n')

    order = (
        (('1 *', '*', '1'), {'name': 'mul'}),
        (('1 *', '*', '1'), {'name': 'rmul'}),
        (('1 *', '/', '(other.length ** 2)'), {'name': 'truediv'}),
        (('1 /', '*', '(self.length ** 2)'), {'name': 'rtruediv'}),
    )
    for t, d in order:
        exec(template.format(*t, **d))

    del template, order, t, d

    # operation implementation: end #

    @classmethod
    def from_ld(cls, vector):
        return cls(*vector)

    @classmethod
    def from_xy(cls, vector):
        x, y = vector
        l = sqrt(x ** 2 + y ** 2)
        if l == 0:
            return cls(0, 0)
        d = round(180 * acos(x / l) / pi)
        if y < 0:
            d = 360 - d
        return cls(l, d)

    length = property(itemgetter(0))
    direction = property(itemgetter(1))

    @property
    def x(self):
        d = pi * self.direction / 180
        return self.__class__.round(self.length * cos(d))

    @property
    def y(self):
        d = pi * self.direction / 180
        return self.__class__.round(self.length * sin(d))


XYVector.to_ld = (lambda self: LDVector.from_xy(self))
XYVector.to_xy = (lambda self: XYVector.from_xy(self))
LDVector.to_xy = (lambda self: XYVector.from_ld(self))
LDVector.to_ld = (lambda self: LDVector.from_ld(self))


if __name__ == "__main__":
    xyv1 = XYVector(1, 1)
    xyv2 = XYVector(-1, 1)
    ldv1 = LDVector.from_xy((-1, -1))
    ldv2 = LDVector.from_xy((1, -1))
    codes = ('xyv1', 'xyv2', 'ldv1', 'ldv2')
    tmpls = (
        'xyv1 {0} xyv2', 'xyv1 {0} ldv1', 'xyv1 {0} ldv2',
        'xyv2 {0} xyv1', 'xyv2 {0} ldv1', 'xyv2 {0} ldv2',
        'ldv1 {0} xyv1', 'ldv1 {0} xyv2', 'ldv1 {0} ldv2',
        'ldv2 {0} xyv1', 'ldv2 {0} xyv2', 'ldv2 {0} ldv1',
    )
    order = tuple('+-*/')
    for e in order:
        codes += tuple(map(
            (lambda tmpl: tmpl.format(*e)),
            tmpls
        ))
    for code in codes:
        print(f'{code} = {eval(code)}')

Execution result:

xyv1 = XYVector(x=1, y=1)
xyv2 = XYVector(x=-1, y=1)
ldv1 = LDVector(length=1.414, direction=225)
ldv2 = LDVector(length=1.414, direction=315)
xyv1 + xyv2 = XYVector(x=0, y=2)
xyv1 + ldv1 = XYVector(x=0.0, y=0.0)
xyv1 + ldv2 = XYVector(x=2.0, y=0.0)
xyv2 + xyv1 = XYVector(x=0, y=2)
xyv2 + ldv1 = XYVector(x=-2.0, y=0.0)
xyv2 + ldv2 = XYVector(x=0.0, y=0.0)
ldv1 + xyv1 = LDVector(length=0, direction=0)
ldv1 + xyv2 = LDVector(length=2.0, direction=180)
ldv1 + ldv2 = LDVector(length=2.0, direction=270)
ldv2 + xyv1 = LDVector(length=2.0, direction=0)
ldv2 + xyv2 = LDVector(length=0, direction=0)
ldv2 + ldv1 = LDVector(length=2.0, direction=270)
xyv1 - xyv2 = XYVector(x=2, y=0)
xyv1 - ldv1 = XYVector(x=2.0, y=2.0)
xyv1 - ldv2 = XYVector(x=0.0, y=2.0)
xyv2 - xyv1 = XYVector(x=-2, y=0)
xyv2 - ldv1 = XYVector(x=0.0, y=2.0)
xyv2 - ldv2 = XYVector(x=-2.0, y=2.0)
ldv1 - xyv1 = LDVector(length=2.828, direction=225)
ldv1 - xyv2 = LDVector(length=2.0, direction=270)
ldv1 - ldv2 = LDVector(length=2.0, direction=180)
ldv2 - xyv1 = LDVector(length=2.0, direction=270)
ldv2 - xyv2 = LDVector(length=2.828, direction=315)
ldv2 - ldv1 = LDVector(length=2.0, direction=0)
xyv1 * xyv2 = 0.0
xyv1 * ldv1 = -2.0
xyv1 * ldv2 = 0.0
xyv2 * xyv1 = 0.0
xyv2 * ldv1 = 0.0
xyv2 * ldv2 = -2.0
ldv1 * xyv1 = -1.999
ldv1 * xyv2 = 0.0
ldv1 * ldv2 = 0.0
ldv2 * xyv1 = -0.0
ldv2 * xyv2 = -1.999
ldv2 * ldv1 = 0.0
xyv1 / xyv2 = 0.0
xyv1 / ldv1 = -1.0
xyv1 / ldv2 = 0.0
xyv2 / xyv1 = 0.0
xyv2 / ldv1 = 0.0
xyv2 / ldv2 = -1.0
ldv1 / xyv1 = -1.0
ldv1 / xyv2 = 0.0
ldv1 / ldv2 = 0.0
ldv2 / xyv1 = -0.0
ldv2 / xyv2 = -1.0
ldv2 / ldv1 = 0.0

Recommended Posts

The story of making an immutable mold
The story of making Python an exe
The story of an error in PyOCR
[Pythonista] The story of making an action to copy selected text
The story of sys.path.append ()
The story of making the Mel Icon Generator version2
The story of making a lie news generator
The story of making a mel icon generator
The story of building Zabbix 4.4
[Apache] The story of prefork
The story of making a music generation neural network
The story of making a question box bot with discord.py
The story of Python and the story of NaN
The story of participating in AtCoder
Get the attributes of an object
The story of the "hole" in the file
The story of remounting the application server
The story of writing a program
The story of making a standard driver for db with python.
The story of making a module that skips mail with python
The story of trying to reconnect the client
The story of verifying the open data of COVID-19
The story of adding MeCab to ubuntu 16.04
The story of manipulating python global variables
The story of trying deep3d and losing
The story of deciphering Keras' LSTM model.predict
The story of blackjack A processing (python)
The story of pep8 changing to pycodestyle
Make the default value of the argument immutable
The story of making a sound camera with Touch Designer and ReSpeaker
The story of the escape probability of a random walk on an integer grid
The story of making a package that speeds up the operation of Juman (Juman ++) & KNP
Be careful of the type when making an image mask with Numpy
The story of making a tool to load an image with Python ⇒ save it as another name
The story of doing deep learning with TPU
The story of low learning costs for Python
The story of making a box that interconnects Pepper's AL Memory and MQTT
The story of making a web application that records extensive reading with Django
Image processing? The story of starting Python for
The story of finding the optimal n in N fist
The story of misreading the swap line of the top command
The story of reading HSPICE data in Python
The story of trying Sourcetrail × macOS × VS Code
The story of viewing media files in Django
The story of making a Line Bot that tells us the schedule of competitive programming
[Small story] Download the image of Ghibli immediately
The story of making soracom_exporter (I tried to monitor SORACOM Air with Prometheus)
The story of moving from Pipenv to Poetry
The story of launching a Minecraft server from Discord
A story that reduces the effort of operation / maintenance
The story of Python without increment and decrement operators.
The story of stopping the production service with the hostname command
An Introduction of the Cutting Edge Technologies-AI, ML, DL
The story of replacing Nvidia GTX 1650 with Linux Mint 20.1.
The story of building the fastest Linux environment in the world
The story of Hash Sum mismatch caused by gcrypto20
The story of sharing the pyenv environment with multiple users
The story of FileNotFound in Python open () mode ='w'
Make the default value of the argument immutable (article explanation)
[Golang] Specify an array in the value of map
A story about changing the master name of BlueZ