Table of Contents

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 coordinates 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 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.

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 to debug nor makes sense to a 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 built-in 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'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!

Share this post:

Leave a comment

Similar Posts


Fake it before you make it.