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
1 Answer 1
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.
-
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 ownmagnitude
generic function. \$\endgroup\$Gareth Rees– Gareth Rees2014年09月19日 13:32:05 +00:00Commented Sep 19, 2014 at 13:32
Explore related questions
See similar questions with these tags.