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
asked Jun 21, 2014 at 5:00
\$\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.

answered Jun 21, 2014 at 11:44
\$\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\$ Commented Sep 19, 2014 at 13:32

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.