3
\$\begingroup\$

Recently I've been wondering about ways of implementing objects in code that can be represented by other primitives. An example of this is a Vector, which can be represented by a Vector\$N\$D where \$N\$ is the dimension of vector, or it can be represented by an \$N\$-tuple.

The reason this is causing me so much trouble is because I can't decide whether to provide a class that implements all the functions that can be applied to a vector as methods, or to provide a bunch of functions that aren't bounded to a class and can act upon any vector-like object (a vector class or tuple).

The code below is one solution I had to the problem (not the optimal one I don't think, just one I was messing around with). It basically implements a Vector class as an interface that can be used as a class with methods (i.e., Vector2D(1, 2).magnitude()) or can be used as a collection of "static methods" (using the term dubiously) to act on non Vector objects (i.e. Vector2D.magnitude((1, 2))).

The code I came up with feels very un-pythonic however. It requires the usage of calling methods as static methods from the class and seems kind of messy in general. What could I do to solve my dilemma whilst also making this code more pythonic?

import math

class VectorMeta(type):

    component_names = ('x', 'y', 'z', 'w', 'u', 'v')

    def __new__(metacls, name, bases, kwargs):
        degree = kwargs.get('_degree')
        if degree:        
            for i in range(degree):
                def _getter(self, i=i):
                    return self[i]
                def _setter(self, val, i=i):
                    self[i] = val

                prop = property(fget=_getter, fset=_setter)
                kwargs[metacls.component_names[i]] = prop

        return super().__new__(metacls, name, bases, kwargs)

class _baseVector(metaclass=VectorMeta):

    def __init__(self, *args):
        if len(args) == self._degree:
            self._components = list(args)
        elif len(args) == 1 and isinstance(args[0], (list, tuple)):
            self._components = list(args[0])
        else:
            raise ValueError("Too many components for vector of length %i." % self._degree)

    def _range(self):
        return range(len(self))

    def __len__(self):
        """
        X.__len__() <==> len(X)
        """
        return len(self._components)

    def __iter__(self):
        """
        X.__iter__() <==> iter(X)
        """
        for component in self._components:
            yield component

    def __getitem__(self, key):
        """
        X.__getitem__(key) <==> X[key]
        """
        return self._components[key]

    def __setitem__(self, key, val):
        """
        X.__setitem__(key, val) <==> X[key] = val
        """
        self._components[key] = val

    def __repr__(self):
        """
        X.__repr__() <==> repr(X)
        """
        return "%s%s" % (
            self.__class__.__name__,
            str(tuple(self._components))
            )

    def __add__(self, other):
        """
        X.__add__(Y) <==> X + Y
        """
        _baseVector._diff_len_err(self, other, 'add')
        return self.__class__([self[i] + other[i] for i in _baseVector._range(self)])
    __radd__ = __add__

    def __sub__(self, other):
        """
        X.__sub__(Y) <==> X - Y
        """
        _baseVector._diff_len_err(self, other, 'subtract')
        return self.__class__([self[i] - other[i] for i in _baseVector._range(self)])

    def __rsub__(self, other):
        """
        X.__rsub__(Y) <==> Y - X
        """
        _baseVector._diff_len_err(self, other, 'subtract')
        return self.__class__([other[i] - self[i] for i in _baseVector._range(self)])

    def __mul__(self, other):
        """
        X.__mul__(Y) <==> X * Y
        """
        _baseVector._non_scalar_err(self, other, 'multiply')
        return self.__class__([other*self[i] for i in _baseVector._range(self)])
    __rmul__ = __mul__

    def __truediv__(self, other):
        """
        X.__truediv__(Y) <==> X / Y
        """
        _baseVector._non_scalar_err(self, other, 'divide')
        return self.__class__([self[i]/other for i in _baseVector._range(self)])

    def __floordiv__(self, other):
        """
        X.__floordiv__(Y) <==> X // Y
        """
        _baseVector._non_scalar_err(self, other, 'divide')
        return self.__class__([self[i]/other for i in _baseVector._range(self)])

    add = __add__
    sub = __sub__
    mul = __mul__
    div = __truediv__
    floordiv = __floordiv__

    def cast_to_ints(self):
        """
        Cast the components of the vector to integer values.
        """
        return self.__class__([int(self[i]) for i in _baseVector._range(self)])

    def dot_product(self, other):
        """
        Calculate the dot product of two vectors.
        """
        _baseVector._diff_len_err(self, other, 'perform dot product on')
        return sum([self[i]*other[i] for i in _baseVector._range(self)])

    def magnitude(self):
        """
        Calculate the magnitude of a vector.
        """
        return math.sqrt(sum([self[i]*self[i] for i in range(len(self))]))

    def magnitude_sqrd(self):
        """
        Calculate the squared magnitude of a vector.
        """
        return sum([self[i]*self[i] for i in range(len(self))])

    def normalize(self):
        """
        Normalize a vector (find the unit vector with the same direction).
        """
        magnitude = _baseVector.magnitude(self)
        return self.__class__([self[i]/magnitude for i in range(len(self))])

    def project(self, other):
        """
        Project a vector or vector-like object onto another.
        """
        other_norm = _baseVector.normalize(other)
        a = _baseVector.dot_product(self, other_norm)
        return _baseVector.mul(other_norm, a)

    def _diff_len_err(self, other, action):
        if len(self) != len(other):
            raise ValueError("Cannot %s vectors of different "
                             "length (Got lengths %i and %i)." % \
                             (action, len(self), len(other))
                             )

    def _non_scalar_err(self, other, action):
        if other.__class__ not in {int, float}:
            raise ValueError("Cannot %s vector by non-scalar type %s." % \
                             (action, type(other))
                             )

class Vector2D(_baseVector):
    _degree = 2

    def angle_between(self, other):
        dot_prod = Vector2D.dot_product(self, other)

        mag_self = Vector2D.magnitude(self)
        mag_other = Vector2D.magnitude(other)

        return math.acos(dot_prod/(mag_self*mat_other))

    # 2D vector stuff

class Vector3D(_baseVector):
    _degree = 3

    def cross_product(self, other):
        ax, ay, az = self
        bx, by, bz = other
        return self.__class__([
            ay*bz - az*by,
            az*bx - ax*bz,
            ax*by - ay*bz
            ])

    # 3D vector stuff

class Quaternion(_baseVector):
    _degree = 4

    # Quaternion implementation stuff
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

In its standard library, Python itself prefers using a free-ranging function that may call a method on the class. For instance, len() calls o.__length__, str() calls o.__str__(), etc.

Following this precedent, you can design your module with a magnitude() function that will call o.magnitude() method on the class, or cast it to the appropriate type when given collection objects of the appropriate length.

If you're concerned about namespacing, you can put your code in a module, so that you have:

vector.magnitude(x, y)
vector.magnitude(v)
vector.Vector2D(x, y).magnitude()

and so on.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Python already has a generic function abs, which dispatches to the __abs__ method. So it would make sense to use that instead of adding your own magnitude generic function. \$\endgroup\$ Sep 19, 2014 at 13:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.