Author: Nitin | Published: Sat, 24 Aug, 2019
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 coordinates of the Point, (x, y) from the origin.
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 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 the built-in 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 instances of the Vector class behave like built-in types.
I am listing out some behaviors of the Vector class with no special methods:
>>> v = Vector(3, 4)
>>> v
<__main__.Vector object at 0x7f60a9355c88>
The above representation neither helps to debug nor makes sense to a user of the Vector class.
>>> 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.
>>> 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.
We have already seen the problem earlier in this post.
>>> 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.
>>> 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.
>>> 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 built-in types or in other words, the way we expect it to behave.
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.
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.
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
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.
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.
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.
Phew!, that was a long post. I believe you would have gained some useful insights into Python's special methods. I have not implemented the special method that would allow checking the 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 the DataModel chapter of The Python Language Reference
That's it, readers, until next time! Happy coding Python!
Leave a comment