Vector Take #1" Vector2d Compatible


In [88]:
from array import array
import reprlib
import math
import numbers
import functools
import operator
import itertools

class Vector:
    typecode = 'd'
    
    def __init__(self, components):
        self._components = array(self.typecode, components)
        
    def __iter__(self):
        return iter(self._components)
    
    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)
    
    def __str__(self):
        return str(tuple(self))
    
    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
               bytes(self._components))
    
    def __eq__(self, other):
        return (len(self) == len(other) and
                all(a == b for a, b in zip(self, other)))
    
    def __hash__(self):
        hashes = (hash(x) for x in self._components)
        return functools.reduce(operator.xor, hashes, 0)
    
    def __abs__(self):
        return math.sqrt(sum(x * x) for x in self)
    
    def __bool__(self):
        return bool(abs(self))
    
    def __len__(self):
        return len(self._components)
    
    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{.__name__} indices must be integers'
            raise TypeError(msg.format(cls))
            
    shortcut_names = 'xyzt'
    
    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self._components):
                return self._components[pos]
        msg = '{.__name__!r} object has no atttribute {!r}'
        raise AttributeError(msg.format(cls, name))
        
    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in cls.shortcut_names:
                error = 'readonly attribute {attr_name!r}'
            elif name.islower():
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''
            if error:
                msg = error.format(cls_name=cls.__name__, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)
    
    def angle(self, n):
        r = math.sqrt(sum(x * x for x in self[n:]))
        a = math.atan2(r, self[n-1])
        if (n == len(self) - 1) and (self[-1] < 0):
            return math.pi * 2 - a
        else:
            return a
        
    def angles(self):
        return (self.angle(n) for n in range(1, len(self)))
    
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('h'): # hyperspherical coordinates
            fmt_spec = fmt_spec[:-1]
            coords = itertools.chain([abs(self)], self.angles())
            outer_fmt = '<{}>'
        else:
            coords = self
            outer_fmt = '({})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(', '.join(components))
        
    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)

In [2]:
Vector([3.1, 4.2])


Out[2]:
Vector([3.1, 4.2])

In [3]:
Vector((3, 4, 5))


Out[3]:
Vector([3.0, 4.0, 5.0])

In [4]:
Vector(range(10))


Out[4]:
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

Vector Take # 2: A Sliceable Sequence


In [8]:
v1 = Vector([3, 4, 5])

In [9]:
len(v1)


Out[9]:
3

In [10]:
v1[0], v1[-1]


Out[10]:
(3.0, 5.0)

In [11]:
v7 = Vector(range(7))
v7[1:4]


Out[11]:
array('d', [1.0, 2.0, 3.0])

How Slicing Works


In [12]:
class MySeq:
    def __getitem__(self, index):
        return index

In [13]:
s = MySeq()
s[1]


Out[13]:
1

In [14]:
s[1:4]


Out[14]:
slice(1, 4, None)

In [16]:
s[1:4:2]


Out[16]:
slice(1, 4, 2)

In [17]:
s[1:4:2, 9]


Out[17]:
(slice(1, 4, 2), 9)

In [18]:
s[1:4:2, 7:9]


Out[18]:
(slice(1, 4, 2), slice(7, 9, None))

In [19]:
slice


Out[19]:
slice

In [21]:
print(dir(slice))


['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']

In [22]:
help(slice.indices)


Help on method_descriptor:

indices(...)
    S.indices(len) -> (start, stop, stride)
    
    Assuming a sequence of length len, calculate the start and stop
    indices, and the stride length of the extended slice described by
    S. Out of bounds indices are clipped in a manner consistent with the
    handling of normal slices.


In [23]:
slice(None, 10, 2).indices(5)


Out[23]:
(0, 5, 2)

In [24]:
slice(-3, None, None).indices(5)


Out[24]:
(2, 5, 1)

In [39]:
v7 = Vector(range(7))

In [40]:
v7[-1]


Out[40]:
6.0

In [41]:
v7[1:4]


Out[41]:
Vector([1.0, 2.0, 3.0])

In [42]:
v7[-1:]


Out[42]:
Vector([6.0])

In [43]:
v7[1,2]


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-43-82fccfd8982a> in <module>()
----> 1 v7[1,2]

<ipython-input-38-94191683b821> in __getitem__(self, index)
     45         else:
     46             msg = '{cls.__name__} indices must be integers'
---> 47             raise TypeError(msg.format(cls=cls))
     48 
     49     @classmethod

TypeError: Vector indices must be integers

Vector Take #3: Dynamic Attribute Access


In [45]:
v = Vector(range(10))
v.x


Out[45]:
0.0

In [46]:
v.y, v.z, v.t


Out[46]:
(1.0, 2.0, 3.0)

In [69]:
v = Vector([0.0, 1.0, 2.0, 3.0, 4.0])
v.x


Out[69]:
0.0

In [71]:
v.x = 10


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-71-8101ea78b02a> in <module>()
----> 1 v.x = 10

<ipython-input-67-9d16dfd6f772> in __setattr__(self, name, value)
     67             if error:
     68                 msg = error.format(cls_name=cls.__name__, attr_name=name)
---> 69                 raise AttributeError(msg)
     70         super().__setattr__(name, value)
     71 

AttributeError: readonly attribute 'x'

In [65]:
v


Out[65]:
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

Vector Take #4: Hashing and a Faster ==


In [72]:
2 * 3 * 4 * 5


Out[72]:
120

In [73]:
import functools

In [75]:
functools.reduce(lambda a,b: a*b, range(1,6))


Out[75]:
120

In [76]:
n = 0
for i in range(1, 6):
    n ^= i

In [77]:
n


Out[77]:
1

In [78]:
functools.reduce(lambda a, b: a^b, range(6))


Out[78]:
1

In [79]:
import operator

In [80]:
functools.reduce(operator.xor, range(6))


Out[80]:
1

Vector Take #5: Formatting


In [95]:
v = Vector((float(x) for x in range(1,10)))

In [96]:
v


Out[96]:
Vector([1.0, 2.0, 3.0, 4.0, 5.0, ...])

In [97]:
format(v,'h')


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-97-be720ac1b41f> in <module>()
----> 1 format(v,'h')

<ipython-input-88-309e8baa434a> in __format__(self, fmt_spec)
     94         if fmt_spec.endswith('h'): # hyperspherical coordinates
     95             fmt_spec = fmt_spec[:-1]
---> 96             coords = itertools.chain([abs(self)], self.angles())
     97             outer_fmt = '<{}>'
     98         else:

<ipython-input-88-309e8baa434a> in __abs__(self)
     37 
     38     def __abs__(self):
---> 39         return math.sqrt(sum(x * x) for x in self)
     40 
     41     def __bool__(self):

TypeError: a float is required

In [ ]: