Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A vector class in pure python.
"""
The MIT License (MIT)
Copyright (c) 2015 Mat Leonard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import math
class Vector(object):
def __init__(self, *args):
""" Create a vector, example: v = Vector(1,2) """
if len(args)==0: self.values = (0,0)
else: self.values = args
def norm(self):
""" Returns the norm (length, magnitude) of the vector """
return math.sqrt(sum( comp**2 for comp in self ))
def argument(self):
""" Returns the argument of the vector, the angle clockwise from +y."""
arg_in_rad = math.acos(Vector(0,1)*self/self.norm())
arg_in_deg = math.degrees(arg_in_rad)
if self.values[0]<0: return 360 - arg_in_deg
else: return arg_in_deg
def normalize(self):
""" Returns a normalized unit vector """
norm = self.norm()
normed = tuple( comp/norm for comp in self )
return Vector(*normed)
def rotate(self, *args):
""" Rotate this vector. If passed a number, assumes this is a
2D vector and rotates by the passed value in degrees. Otherwise,
assumes the passed value is a list acting as a matrix which rotates the vector.
"""
if len(args)==1 and type(args[0]) == type(1) or type(args[0]) == type(1.):
# So, if rotate is passed an int or a float...
if len(self) != 2:
raise ValueError("Rotation axis not defined for greater than 2D vector")
return self._rotate2D(*args)
elif len(args)==1:
matrix = args[0]
if not all(len(row) == len(v) for row in matrix) or not len(matrix)==len(self):
raise ValueError("Rotation matrix must be square and same dimensions as vector")
return self.matrix_mult(matrix)
def _rotate2D(self, theta):
""" Rotate this vector by theta in degrees.
Returns a new vector.
"""
theta = math.radians(theta)
# Just applying the 2D rotation matrix
dc, ds = math.cos(theta), math.sin(theta)
x, y = self.values
x, y = dc*x - ds*y, ds*x + dc*y
return Vector(x, y)
def matrix_mult(self, matrix):
""" Multiply this vector by a matrix. Assuming matrix is a list of lists.
Example:
mat = [[1,2,3],[-1,0,1],[3,4,5]]
Vector(1,2,3).matrix_mult(mat) -> (14, 2, 26)
"""
if not all(len(row) == len(self) for row in matrix):
raise ValueError('Matrix must match vector dimensions')
# Grab a row from the matrix, make it a Vector, take the dot product,
# and store it as the first component
product = tuple(Vector(*row)*self for row in matrix)
return Vector(*product)
def inner(self, other):
""" Returns the dot product (inner product) of self and other vector
"""
return sum(a * b for a, b in zip(self, other))
def __mul__(self, other):
""" Returns the dot product of self and other if multiplied
by another Vector. If multiplied by an int or float,
multiplies each component by other.
"""
if type(other) == type(self):
return self.inner(other)
elif type(other) == type(1) or type(other) == type(1.0):
product = tuple( a * other for a in self )
return Vector(*product)
def __rmul__(self, other):
""" Called if 4*self for instance """
return self.__mul__(other)
def __div__(self, other):
if type(other) == type(1) or type(other) == type(1.0):
divided = tuple( a / other for a in self )
return Vector(*divided)
def __add__(self, other):
""" Returns the vector addition of self and other """
added = tuple( a + b for a, b in zip(self, other) )
return Vector(*added)
def __sub__(self, other):
""" Returns the vector difference of self and other """
subbed = tuple( a - b for a, b in zip(self, other) )
return Vector(*subbed)
def __iter__(self):
return self.values.__iter__()
def __len__(self):
return len(self.values)
def __getitem__(self, key):
return self.values[key]
def __repr__(self):
return str(self.values)
@josephduchesne

This comment has been minimized.

Copy link

@josephduchesne josephduchesne commented Aug 10, 2015

Hi Mat, would you mind attaching a license to this Vector class. It's really handy but without a license I'd have to write my own class in order to use it in the project I'm working on, since by default code published on the internet isn't licensed for use at all (I can read this, but not use it). MIT is my favourite for little utilities like this, but obviously its up to you. Thanks!

@mcleonard

This comment has been minimized.

Copy link
Owner Author

@mcleonard mcleonard commented Aug 10, 2015

Thanks for using this. I added the MIT license. Cheers!

@fuzzy-focus

This comment has been minimized.

Copy link

@fuzzy-focus fuzzy-focus commented Aug 25, 2015

I think there is an error on line 62:

if not all(len(row) == len(v) for row in matrix) or not len(matrix)==len(self):
                                       ^------- This right here

it should be:

 if not all(len(row) == len(matrix) for row in matrix) or not len(matrix)==len(self):
@DylanDmitri

This comment has been minimized.

Copy link

@DylanDmitri DylanDmitri commented Sep 9, 2015

more pythonic to use type(other) in (int, float) rather than type(other) == type(1) or type(other) == type(1.0)

@insolor

This comment has been minimized.

Copy link

@insolor insolor commented Dec 18, 2015

@DylanDmitri, even more pythonic to use isinstance(other, (int, float))

@Sasszem

This comment has been minimized.

Copy link

@Sasszem Sasszem commented Feb 9, 2016

But still looks like REALLY cool.

@josiest

This comment has been minimized.

Copy link

@josiest josiest commented Mar 6, 2016

@insolor, that's arguable. since type(other) in (int, float) looks cleaner (parentheses next to each other look ugly) imho.

@jordvisser

This comment has been minimized.

Copy link

@jordvisser jordvisser commented Jan 16, 2017

Hi Mat,

Is it possible to change all class-creation calls from Vector() to self.__class__() ?
This will come in handy when the class will be used as a base class, so that when the base class calls an creation of a new object the new class which uses the Vector-class as is base class is called.

I'm fairly new to github, should I be changing this myself and submit the change for you to review?

Thanks!

@Marodan

This comment has been minimized.

Copy link

@Marodan Marodan commented Jul 20, 2017

In line 115 t's supposed to be truediv instead of div.

@gokudomatic

This comment has been minimized.

Copy link

@gokudomatic gokudomatic commented Oct 8, 2017

Thank you. I was looking for a small pythonic vector class that didn't require numpy or any other fat library.

@ghedin

This comment has been minimized.

Copy link

@ghedin ghedin commented Jan 18, 2018

Thank you, that's very simple and helpful. 👍

I've added a method to get the angle between two vectors => https://gist.github.com/ghedin/ad776edeb11ef12a7264237d7eb39047/revisions

@rshayduk

This comment has been minimized.

Copy link

@rshayduk rshayduk commented Aug 22, 2018

What if I want to use coordinates system, other than Cartesian? In this class, when you define coordinates pretending that you have defined a vector, in fact, you did not. You must have some coordinates system in mind. In this class, it is only Cartesian.

@cocoaaa

This comment has been minimized.

Copy link

@cocoaaa cocoaaa commented Jul 19, 2019

Hi, thank you for sharing the code! I'd suggest checking the type of other using isinstance. Currently, it doesn't handle the multiplication of a Vector object and a numpy.float object correctly, since np.float object doesn't pass the type checking as is now (which only python float and int objects pass). isinstance would give a better way to handle this. How about replacing these two lines as following?

  • if type(other) == type(self): --> if isinstance(other, type(self)):
  • elif type(other) == type(1) or type(other) == type(1.0): --> elif isinstance(other, (float, int)):
@oziphantom

This comment has been minimized.

Copy link

@oziphantom oziphantom commented Aug 21, 2019

For python 3 you need to change __div__ to __truediv__

@marengohue

This comment has been minimized.

Copy link

@marengohue marengohue commented Sep 19, 2019

What do you think about including some helper properties to make it seem like an actual vector?

    @property
    def x(self):
        """ Returns the first vector component """
        return self[0]

    @property
    def y(self):
        """ Returns the second vector component """
        return self[1]

    @property
    def z(self):
        """ Returns the third vector component """
        return self[2]
        
    @property
    def w(self):
        """ Returns the fourth vector component """
        return self[3]

so that it can be used like this:

v = Vector(10, 10)
print v.x
@monoceros84

This comment has been minimized.

Copy link

@monoceros84 monoceros84 commented Oct 6, 2020

What if I want to use coordinates system, other than Cartesian? In this class, when you define coordinates pretending that you have defined a vector, in fact, you did not. You must have some coordinates system in mind. In this class, it is only Cartesian.

It's quite some while ago but today I have extended/changed this nice class to use polar coordinates. The idea is to still use cartesian internally but add a user interface for polars. The first 3 methods (__init__, norm and argument) will be replaced by the following:

def __init__(self, vector, polar=False, usedegrees=False):
    """ Create a vector, example: v = Vector( (1,2) )
        may also be given in polar coodinates in 2D or 3D and in radians or degrees (angle is counter-clockwise from +x)"""
    if not vector: self.values = (0,0)
    else:
        if polar:
            self.values = self._cartesian(vector, usedegrees)
        else:
            self.values = vector
    
def norm(self, polarcoordinates=False):
    """ Returns the norm (length, magnitude) of the vector 
        if polarcoordinates: Returns the 2D norm / radius (length, magnitude) of the vector in x-y-plane"""
    if polarcoordinates:
        return math.sqrt(self.values[0]**2 + self.values[1]**2 )
    else:
        return math.sqrt(sum( comp**2 for comp in self ))

def argument(self, polarcoordinates=False, usedegrees=False):
    """ Returns the argument of the vector, the angle counter-clockwise from +x."""
    if polarcoordinates:
        arg_in_rad = math.acos(self.values[0]/self.norm(True))
    else:
        arg_in_rad = math.acos(self.values[0]/self.norm())
    if self.values[1]<0: arg_in_rad = -arg_in_rad
    if usedegrees:
        return math.degrees(arg_in_rad)
    else:
        return arg_in_rad

def polar(self, usedegrees=False):
    """ Returns the vector in polar coodinates."""
    if len(self)==2:
        coords = (self.norm(True), self.argument(True, usedegrees))
    elif len(self)==3:
        coords = (self.norm(True), self.argument(True, usedegrees), self.values[2])
    else:
        raise ValueError("Polar coodinates actually not defined for greater than 3D vectors.")
    return self.__class__(coords)

def _cartesian(self, polarvec, usedegrees=False):
    """ Returns the vector in cartesian coodinates, expecting the stored vector to be in polar coodinates."""
    if usedegrees: phi = math.radians(polarvec[1])
    else: phi = polarvec[1]
    if len(polarvec)==2:
        coords = (polarvec[0] * math.cos(phi), polarvec[0] * math.sin(phi))
    elif len(polarvec)==3:
        coords = (polarvec[0] * math.cos(phi), polarvec[0] * math.sin(phi), polarvec[2])
    else:
        raise ValueError("Polar coodinates actually not defined for greater than 3D vectors.")
    return coords

This changes the class initialization to provide a tuple or list instead of comma separated arbitrary many parameters. Otherwise I was not able to add the optional arguments polar and usedegrees.
One important change needs to be done as well then: every occurance of Vector(*...) must become a Vector(...) or even better a self.__class__(...).

Attention: in the same instance I have changed to definition of the argument method. It now counts counter-clockwise from the +x axis. Before it was clock-wise from the +y axis (which in my world has never been used so far).

Usage is simple: if you want to define polar coordinates, add True after the vector definition: Vector((1,math.pi/4), True) defines a 2D vector of length 1 with angle pi/4=45°.
If you want to read a vector in polars, use the polar() method: Vector([1,1]).polar(True) reads with angle in degrees.

@marengohue

This comment has been minimized.

Copy link

@marengohue marengohue commented Oct 12, 2020

To be honest, mashing the polar coordinates it in the same class doesn't seem that good. feels like this is supposed to be a separate class

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.