Making A Vector behave like builtin
Posted on Sat 24 August 2019 in Python
What is a Vector?
A Vector is a quantity that has a magnitude and direction like those used in math and physics.
For example, a 2-dimensional positional vector V -> (x, y) is a line from the origin(0, 0) to Point (x, y). It could be represented as an instance Vector(x,y)
of class Vector
like so:
Vector(x, y)
where x, y are the x and y co-ordinates of the Point, (x, y) from the origin.
Python Special Methods
You must be familiar with Python builtin types such as int, str and so on. We could perform certain operations on these
types. For example, when we use the +
operator on two integers, we get the sum of the integers as the result. Similarly
when we use the +
operator on two strings, we get the the two strings concatenated together as the result.
>>> 3 + 4
7
>>> "hello" + "world"
'helloworld'
However, if we use the +
operator on a user defined type, Python will throw a TypeError
, because it does not
know what operation to perform for +
, unless the operation is defined using Python special method __add__
.
>>> class Vector:
... def __init__(self, x, y):
... self.x = x
... self.y = y
...
>>> v1 = Vector(3, 4)
>>> v2 = Vector(5, 9)
>>> v1 + v2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'
Special methods like __add__
make a user defined type behave like builtin type. We shall see later in the post,
how the addition operation could be done on the Vector instances without the interpreter throwing the TypeError.
We should not call these special methods directly in our programs. They are meant to be called by the Python interpreter.
The only special method that we might call frequently is the __init__
method - to invoke the initializer
of the superclass in your own __init__
.
In the following sections we shall see how we can make the instances of the Vector class behave like builtin types.
Vector - Behavior without special methods
I am listing out some behaviors of the Vector class with no special methods:
- String representation of the Vector instance
>>> v = Vector(3, 4)
>>> v
<__main__.Vector object at 0x7f60a9355c88>
The above representation neither helps debugging nor makes sense to an user of the Vector class.
- Magnitude of the Vector instance
>>> abs(v)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'Vector'
We are not able to get the size of the Vector instance.
- Boolean value of the Vector instance
>>> bool(v)
True
>>> v1 = Vector(0, 0)
>>> bool(v1)
True
By default, the boolean value of an instance of any user defined class is True. But we expect a Vector with zero
magnitude (v1
) to have a boolean value of False.
- Addition of two Vectors instances
We have already seen the problem earlier in this post.
- Scalar multiplication of a Vector instance
>>> v * 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Vector' and 'int'
When we multiply a Vector by a scalar, it is expected to return a scaled up (increased magnitude) version of the Vector.
- Unpacking a Vector into individual numeric components
>>> x, y = v
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot unpack non-iterable Vector object
We are not able to unpack the Vector instance as it is not iterable.
- Checking the equality of two Vectors
>>> v5 = Vector(3, 4)
>>> v6 = Vector(3, 4)
>>> v5 == v6
False
In the above example, although the numeric components of the two Vectors are the same, their equality check returns False. This is because by default, Python returns True only if the Vectors are the same exact objects like so:
>>> v7 = v5
>>> v7 == v5
True
However, we expect v5
and v6
to return True on equality check.
Now we shall add certain special methods to the Vector class to make it behave like builtin types or in other words, the way we expect it to behave.
Adding special methods to Vector class
String representation of the Vector instance
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
>>> v
Vector(3, 4)
The Python interpreter, debugger or repr builtin function invokes the __repr__
special
method. So we can use this special method to return a human friendly
representation of the Vector instance.
- Magnitude of the Vector instance
from math import hypot
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
>>> abs(v)
5.0
We have implemented the special method __abs__
.
Python invokes the special method __abs__
when the builtin function abs
is
used on the Vector instance.
- Boolean value of the Vector instance
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
def __bool__(self):
"""
Boolean value of the Vector
:return: Returns False, if the magnitude of the Vector is 0, True otherwise
"""
return bool(abs(self))
>>> v1 = Vector(0, 0)
>>> bool(v1)
False
We have implemented the __bool__
special method, which is invoked by the Python
interpreter when the builtin function bool is used on the Vector instance. This special method
is also invoked if we use the Vector instance in any boolean context like if
or while
statements.
>>> if v1:
... print(f"{v1} is Truthy")
... else:
... print(f"{v1} is Falsy")
...
Vector(0, 0) is Falsy
- Addition of two Vectors instances
To add two Vector instance, we need to implement the __add__
special method like so:
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
def __bool__(self):
"""
Boolean value of the Vector
:return: Returns False, if the magnitude of the Vector is 0, True otherwise
"""
return bool(abs(self))
def __add__(self, other):
"""
Adds two Vectors
:return: Returns a new Vector which is the sum of the given Vectors
"""
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
>>> v1 = Vector(3, 4)
>>> v2 = Vector(5, 8)
>>> v1 + v2
Vector(8, 12)
The +
operator is used on two Vector instances, Python interpreter invokes the __add__
special method
.
- Scalar multiplication of a Vector instance
To support scalar multiplication, we need to implement the __mul__
special method.
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
def __bool__(self):
"""
Boolean value of the Vector
:return: Returns False, if the magnitude of the Vector is 0, True otherwise
"""
return bool(abs(self))
def __add__(self, other):
"""
Adds two Vectors
:return: Returns a new Vector which is the sum of the given Vectors
"""
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
"""
Scalar multiplication of the Vector
:param scalar: A value specifying the scale factor
:return: Returns a new Vector which is the scalar multiple of the given Vector
"""
return Vector(self.x * scalar, self.y * scalar)
>>> v1 = Vector(3, 4)
>>> v1 * 3
Vector(9, 12
Python interpreter invokes the special method __mul__
when the *
operator is used to multiply
a Vector instance by a scalar. However, the commutative property of multiplication is not
satisfied. For instance
>>> v1 = Vector(3, 4)
>>> 3 * v1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'
To support the above operation, we need to implement the __rmul__
special method like so:
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
def __bool__(self):
"""
Boolean value of the Vector
:return: Returns False, if the magnitude of the Vector is 0, True otherwise
"""
return bool(abs(self))
def __add__(self, other):
"""
Adds two Vectors
:return: Returns a new Vector which is the sum of the given Vectors
"""
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
"""
Scalar multiplication of the Vector
:param scalar: A value specifying the scale factor
:return: Returns a new Vector which is the scalar multiple of the given Vector
"""
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""
Scalar multiplication of the Vector if the scalar is on the RHS of the multiplication operator
:param scalar: A value specifying the scale factor
:return: Returns a new Vector which is the scalar multiple of the given Vector
"""
return self * scalar
>>> 3 * v1
Vector(9, 12)
We reuse the __mul__
method, instead of duplicating the code.
- Unpacking a Vector into individual numeric components
To unpack a Vector into individual components, we need to make iterable. This can be done by implementing
the __iter__
special method. Python invokes the builtin function iter()
when we try to
unpack the Vector instance or when we try to loop over it.
class Vector:
def __init__(self, x, y):
"""
Initializes a 2-D Vector
:param x: x co-ordinate
:param y: y co-ordinate
"""
self.x = x
self.y = y
def __repr__(self):
"""
String representation of the Vector
:return: Returns string representation of the Vector instance
"""
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""
Magnitude of the Vector
:return: Returns the Euclidean Distance of the Vector
"""
return hypot(self.x, self.y)
def __bool__(self):
"""
Boolean value of the Vector
:return: Returns False, if the magnitude of the Vector is 0, True otherwise
"""
return bool(abs(self))
def __add__(self, other):
"""
Adds two Vectors
:return: Returns a new Vector which is the sum of the given Vectors
"""
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
"""
Scalar multiplication of the Vector
:param scalar: A value specifying the scale factor
:return: Returns a new Vector which is the scalar multiple of the given Vector
"""
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""
Scalar multiplication of the Vector if the scalar is on the RHS of the multiplication operator
:param scalar: A value specifying the scale factor
:return: Returns a new Vector which is the scalar multiple of the given Vector
"""
return self * scalar
def __iter__(self):
"""
Makes the Vector iterable
:return: Returns an iterator to iterate over.
"""
yield self.x
yield self.y
>>> v1 = Vector(3, 4)
>>> x, y = v1
>>> x
3
>>> y
4
The __iter__
method is implemented as a generator which yields the numeric components of the vector when iterated.
Conclusion
Phew!, that was a long post. I believe, you would have gained some useful insights into Python special methods. I have not implemented the special method that would allow checking equality of two Vectors. I urge you to figure that out and implement that in our Vector class. To understand more about the Python data model and the special methods, I recommend reading DataModel chapter of The Python Language Reference
Thats's it readers, until next time! Happy coding Python!