Python informer

Improve your Python coding skills

Operator overloading

One of our example classes is Matrix, a 2 by 2 matrix.

You can perform operations such as add or multiply on matrices. The most obvious way to do this would be to define an add function:

p = Matrix(1, 2, 3, 4)
q = Matrix(5, 6, 7, 8)
r = p.add(q)

That is ok, but it would be nice if we could write:

p = Matrix(1, 2, 3, 4)
q = Matrix(5, 6, 7, 8)
r = p + q

Well we can! In fact we can override all the arithmetic operators if we wish. In this section we will look at + and *, but the technique can apply to any operator.

Matrix addition

Just to recap the basics of matrix algebra, the sum of two matrices:

$$\begin{pmatrix}a & b\\c & d\end{pmatrix} + \begin{pmatrix}e & f\\g & h\end{pmatrix}$$

is

$$\begin{pmatrix}a + e & b + f\\c + g & d + h\end{pmatrix}$$

Overriding the addition operator

You can override the addition operator for a class by providing an __add__ method:

class Matrix:

    def __init__(self, a, b, c, d):
        self.data = [a, b, c, d]

    def __add__(self, other):
        if isinstance(other, Matrix):
            return Matrix(self.data[0] + other.data[0],
                          self.data[1] + other.data[1],
                          self.data[2] + other.data[2],
                          self.data[3] + other.data[3])
        else:
            return NotImplemented

The __add__ method accepts a parameter other. We first check other is a Matrix. If it is, the method creates a brand new Matrix object whose elements formed by adding the elements of other to self.

If other is not a Matrix, our code doesn’t know how to handle it. In that case we must return NotImplemented. Python can then decide what to do (this is covered in more detail below).

Here is how this is used:

p = Matrix(1, 2, 3, 4)
q = Matrix(5, 6, 7, 8)
r = p + q
print(r)

We create two matrices, p and q. We then perform the calculation p + q. This calls the __add__ method on the first object p, passing the second object r as the other parameter.

The __add__ function returns a Matrix that is the result of the addition, and this gets assigned to r. The result is printed:

[6, 8][10, 12]

Matrix multiplication

Matrix multiplication is a more interesting case, because you can multiply a matrix by another matrix, or alternatively you can multiply it by a scalar (ie an ordinary number).

Multiplying a matrix by a matrix

The product of two matrices:

$$\begin{pmatrix}a & b\\c & d\end{pmatrix} . \begin{pmatrix}e & f\\g & h\end{pmatrix}$$

is

$$\begin{pmatrix}a.e + b.g & a.f + b.h\\c.e + d.g & c.f + d.h\end{pmatrix}$$

Multiplying a matrix by a scalar

You can also multiply a matrix by a scalar (an ordinary number n):

$$\begin{pmatrix}a & b\\c & d\end{pmatrix} . n$$

giving:

$$\begin{pmatrix}a.n & b.n\\c.n & d.n\end{pmatrix}$$

Overriding the multiply operator

Here is a version of the Matrix class with an implementation of __mul__

class Matrix:

    def __init__(self, a, b, c, d):
        self.a = a
        self.b = b
        self.c = c
        self.d = d

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            return Matrix(self.data[0] * other,
                          self.data[1] * other,
                          self.data[2] * other,
                          self.data[3] * other)
        elif isinstance(other, Matrix):
            return Matrix(self.data[0] * other.data[0] + self.data[1] * other.data[1],
                          self.data[0] * other.data[1] + self.data[1] * other.data[3],
                          self.data[2] * other.data[0] + self.data[3] * other.data[1],
                          self.data[2] * other.data[1] + self.data[3] * other.data[3])
        else:
            return NotImplemented

If you look at __mul__, you will see that the first thing we do is to check if other is a scalar. We do this by checking if it is an instance of int or float (you could also check complex if you wanted the Matrix class to support complex number, but we won’t bother in this example).

If the value is a number, we execute the code for the scalar multiplication equation above.

If the value is not a scalar, we check if it is a Matrix, and execute the code for the matrix multiplication equation above.

If the value is neither a number nor a Matrix we return NotImplemented.

Here is an example:

p = Matrix(1, 2, 3, 4)
q = Matrix(5, 6, 7, 8)
print(p*2)
print(p*q)

This prints

[2, 4][6, 8]
[17, 22][39, 50]

as expected.

Reversing the arguments

What if we try to do this:

print(2*p)

Unfortunately our existing code doesn’t quite cope with this situation. We get an error:

TypeError: unsupported operand type(s) for *: 'int' and 'Matrix'

So what has happened here? We are trying to multiply 2*p:

  • Python looks at the first value, 2, which is an int.
  • It calls int.__mul__ passing in the second value p which is a Matrix.
  • Since int is a builtin type, its __mul__ function knows nothing of out Matrix type, so it returns NotImplemented.

You might think that Python would give an error at this point, but actually it tries one last thing:

  • Python checks if the second argument p has a __rmul__ method. If not it gives an error.
  • It calls p.__rmul__ passing in the first value 2.
  • If p.__rmul__ can handle an integer type, the value will be calculated.
  • If not, p.__rmul__ returns NotImplemented and Python gives an error.

So, we can handle this extra case by implementing __rmul__ for our Matrix class:

    def __rmul__(self, other):
        if isinstance(other, (int, float)):
            return Matrix(self.data[0] * other,
                          self.data[1] * other,
                          self.data[2] * other,
                          self.data[3] * other)
        else:
            return NotImplemented

In this case, self is the second operand p, and other is the first operand 2. This is because __rmul__ reverses the arguments.

Since other is an int, our code executes and creates the correct result. In this case, the code for handing numbers is identical in __mul__ and __rmul__ because for matrices p*2 and 2*p are the same. That won’t be true for all data types and all operators of course.

Notice that if both operands are of type Matrix, the case will always be handled by __mul__, so there is no need to handle that case in __rmul__. This is generally true for all data types and operators.

Error checking

What if we try something crazy like:

print(p*'abc')

Our __mul__ code checks the type of the other value. It isn’t a number, it isn’t a Matrix, so we return NotImplemented.

Python will then check if str has an __rmul__ method. It does, but it can’t handle our Matrix type so again it returns NotImplemented.

Python gives an error.

In place operators

There is an additional case to consider, the in place operators such as += and *=. They are covered in the next section

Summary of operators

Here is a summary of the available numerical operators:

Method Symbol
__add__ +
__sub__ -
__mul__ *
__matmul__ @
__truediv__ /
__floordiv__ //
__mod__ %
__divmod__
__pow__ **
__lshift__ <<
__rshift__ >>
__and__ &
__xor__ ^
__or__ |