Making A Vector behave like builtin

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.

Vector image

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!

links

social