HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

A 3-D vector class built on top of numpy.array

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
builttopnumpyarrayclassvector

Problem

I wanted a convenient class to easy access the parameters inside. I am using a lot of math in my game; that's why a wanted to access them through x, y and z (for readability). And this is my outcome.

It is a class which lies on top of numpy array. It basically means that I can control a numpy array trough my class. And using x, y and z for accessing the parameters instead of using indices ([0], [1] or [2]).

For that I overwrite the built in functions.

Example:

a = numpy.array([1,1,1], dtype = "float32")
b = a[0]


to

a = Vector3(1,1,1)
b = a.x


So what I want to know is if this is a good way (I wanted a fast written implementation). Are there any performance issues and is it well written?

```
import numpy as np
from numbers import Number

class Vector3(object):
def __init__(self, x = 0, y = 0, z = 0, dtype = "float32"):
self._data = np.array([x,y,z], dtype = dtype)
self.x = self._data[0]
self.y = self._data[1]
self.z = self._data[2]

def __str__(self):
return str("Vector3({0.x},{0.y},{0.z})".format(self))

def __mul__(self, value):
if isinstance(value, type(self)):
result = self._data * value._data
elif isinstance(value, Number):
result = self._data * value
return type(self)(x = result[0], y = result[1], z = result[2])

def __rmul__(self, value):
return self.__mul__(value) # Kommutativgesetz/commutative

def __add__(self, value):
if isinstance(value, type(self)):
result = self._data + value._data
elif isinstance(value, Number):
result = self._data + value
return type(self)(x = result[0], y = result[1], z = result[2])

def __radd__(self, value):
return self.__add__(value) # Kommutativgesetz/commutative

def __sub__(self, value):
if isinstance(value, type(self)):
result = self._data - value._data
elif isinstance(value, Number

Solution

Your code and text mismatch. You state that you want to use x, y and z, but you don't actually do it. You only use them in the initializer, and then you use them as named parameters when you recreate your object when returning the value. So your code kind of obfuscates that you are working on a numpy array.

Simplify overridden functions

You could also simplify your code some as almost all of the functions does the same:

OPERATIONS = {
    "add" : numpy.add,
    "mul" : numpy.multiply,
    "sub" : numpy.subtract,
    ...
}

def _apply_operation(self, value, operation):

    if isinstance(value, type(self)):
        result = OPERATIONS[operation](value._data, self._data)
    elif isinstance(value, Number):
        result = OPERATIONS[operation](value, self._data)
    return type(self)(*result)


And this could be called like:

def __add__(self, value):
    return self._apply_operation(value, "add")


Another question is why do you use if ... elif, without any else. This does leave for potentially cases where neither of them hit, and this could lead to the result being undefined when returning.

If however you actually mean else instead of elif you simplify the calculation to:

result = OPERATIONS[operation](value._data if isinstance(value, type(self))
                                           else value,
                               self._data)


Or possibly even loose the result and do:

return type(self)(*OPERATIONS[operation](value._data if isinstance(value, type(self))
                                                     else value,
                                         self._data)


Consider adding/changing __repr__ and __str__

In Python it's normal to add spaces after commas, something your __str__ doesn't do. I would change the __str__ and add a __repr__ method like the following:

def __str__(self):
    return str('!r'.format(self))

def __repr__(self):
    return 'Vector3({0}, {1}, {2})'.format(*self._data)


Implement a better test scheme

Your tests are executed but you only have a visual test of them. A much better option would be to use doctest. This would allow you to write tests in the header of each function, and you could be sure they all works as expected.

Something like the following:

def __add__(self, value):
    """Adds the value into self, and returns result as Vector3.

    >>> Vector3(1, 2, 3) + Vector3(2, 4, 6)
    Vector3(3.0, 6.0, 9.0)

    >>> Vector3(1, 2, 3) + 5
    Vector3(6.0, 7.0, 8.0)
    """

    return self._apply_operation(value, "add")


And then in your main code you could simply do:

doctest.testmod()


If all tests pass, you'll see nothing, and it they fail, you'll see why the fail and both the expected and actual results.

Properties

If you want to use x, y and z as aliases into self._data you could use properties which changes self._data like the following:

@property
def x(self):
    """Property x is first element of vector.

    >>> Vector3(10, 20, 30).x
    10.0
    """
    return self._data[0]

@x.setter
def x(self, value):
   self._data[0] = value

...

if __name__ == '__main__':
    doctest.testmod()

    a = Vector3(1, 2, 3)
    a.x = 10.0

    print('a = {}, a.z = {}'.format(a, a.x))


Which would output:

a = Vector3(10.0, 2.0, 3.0), a.z = 10.0


Do however remove the setting of self.x, self.y and self.z from __init__ so you don't have conflicting data variables.

Code Snippets

OPERATIONS = {
    "add" : numpy.add,
    "mul" : numpy.multiply,
    "sub" : numpy.subtract,
    ...
}

def _apply_operation(self, value, operation):

    if isinstance(value, type(self)):
        result = OPERATIONS[operation](value._data, self._data)
    elif isinstance(value, Number):
        result = OPERATIONS[operation](value, self._data)
    return type(self)(*result)
def __add__(self, value):
    return self._apply_operation(value, "add")
result = OPERATIONS[operation](value._data if isinstance(value, type(self))
                                           else value,
                               self._data)
return type(self)(*OPERATIONS[operation](value._data if isinstance(value, type(self))
                                                     else value,
                                         self._data)
def __str__(self):
    return str('!r'.format(self))

def __repr__(self):
    return 'Vector3({0}, {1}, {2})'.format(*self._data)

Context

StackExchange Code Review Q#114409, answer score: 8

Revisions (0)

No revisions yet.