We’ve seen that the usual arithmetic operators +, -, * and / can be used with Python’s numeric data types, and that some of these operators also have meaning when applied to other data types. For example, we can join two strings using +, add a list onto another list using + and so on.
These are examples of operator overloading, in which the same operator such as + has a meaning which depends on its operands (the quantities on which it operates).
Like many object oriented languages, Python allows you to overload operators so that they can modified to suit your own user-defined classes. To do this, we need to know the special names that Python uses to implement its various operators.
There are a large number of operators that can be overloaded. In addition to what we normally think of as operators (arithmetic, comparison and so on), other symbols such as parentheses and brackets are also operators and can be overloaded. In order not to ‘overload’ the reader at this stage, though, we’ll have a look at just the simple arithmetic operators.
Let’s say you want to define a class to represent a mathematical vector. The standard type of vector you have probably encountered in high school mathematics classes is defined as an object that has both magnitude and direction, and, in two or three dimensions at least, can be represented by an arrow in the corresponding space.
We’ll generalize this definition a bit by allowing our vectors to have any number of dimensions. This might sound esoteric, but in fact, we can represent a vector in dimensions by a list containing
numbers. In Python code, a bare-bones vector class would therefore be given by the file Vector.py:
class Vector(object): """a mathematical vector""" def __init__(self, comps): self.comps = comps
Here, comps (short for ‘components’) is assumed to be a Python list.
Now suppose we would like to be able to add two such Vectors using the + operator. That is, we’d like to be able to write code like:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) vec3 = vec1 + vec2
We would like vec3
to be a Vector object with components [6.2, 8, 10, 12].
Operator methods
To do this, we need to understand that Python implements all its operators by running a special method for each one. These methods all have names that begin and end with a double underscore. Thus the statement vec3 = vec1 + vec2
is implemented by running the statement vec3 = vec1.__add__(vec2)
. The method __add__
is the special method that implements the + operator, and Python has built-in definitions of __add__
for many of its built-in data types, as we mentioned above.
If we try to run the code above using the bare-bones Vector class, we’ll get an error that says: TypeError: unsupported operand type(s) for +: ‘Vector’ and ‘Vector’. We are told that the + operator isn’t defined when its operands are Vectors.
To fix this, we need to write our own version of __add__
that does take Vectors as operands. Here’s a modified version of the Vector class that does this:
class Vector(object): """a mathematical vector""" def __init__(self, comps): self.comps = comps def __add__(self, other): if isinstance(other, Vector): if len(other.comps) == len(self.comps): return Vector([self.comps[i] + other.comps[i] for i in range(len(self.comps))]) else: return None else: return None
The overloaded __add__
method is on lines 6 to 14. It takes two arguments: self
and other
. self
, as usual, refers to the object (a Vector) that calls the method, and is the Vector that appears on the left side of the + operator. The other
object is the Vector on the right side of the +.
We want the overloaded + to handle the case where both its operands are Vectors. Since it is defined within the Vector class, self
is guaranteed to be a Vector, so we need check only that the other operand is also a Vector, which we do with the isinstance()
method on line 7. As vector addition is defined only if the two vectors have the same dimension, we check that the lengths of the comps
lists are equal, and if they are, we create and return a new Vector whose comps
list is the sum of the comps
lists in the two operands.
At this point, we reject any + operation that doesn’t satisfy the requirement that both operands are Vectors of the same dimension, by returning None
in all these cases. In practice, we would probably want to make things a bit more user-friendly by printing error messages, but we don’t want to clutter up the code here.
We now test the Vector class with the code:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) vec3 = vec1 + vec2 print(vec3.comps)
We find that it does indeed give us what we want. The components are [6.2, 8, 10, 12].
Now suppose we want to expand the meaning of the + operator so that if its left operand is a Vector and its right operand is a number (int or float), then the Vector’s comps
list is extended by appending the number onto the end of the list, effectively increasing the dimension of the Vector by 1. In this case, the original Vector is changed so we don’t want to create a new vector.
We can do this by modifying __add__
as shown:
class Vector(object): """a mathematical vector""" def __init__(self, comps): self.comps = comps def __add__(self, other): if isinstance(other, Vector): if len(other.comps) == len(self.comps): return Vector([self.comps[i] + other.comps[i] for i in range(len(self.comps))]) else: return None elif isinstance(other, (int, float)): self.comps += [other] return self else: return None
The extra code is on lines 13 to 15. We check that other
is a numeric type (int or float) and, if so, we append other
onto the end of self.comps
, and return self
, which is the original Vector object, now modified with the addition of an extra component.
We can test this code with:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) vec1 + 9 print(vec1.comps)
We now get [1.2, 2, 3, 4, 9] printed out, showing that the component 9 has indeed been added to vec1
.
Right operands calling overloaded operators
Now suppose we want a way of adding an extra element to the beginning of the comps list, rather than the end. A natural way of doing this would be to allow an expression like 10.7 + vec2
, with the new element as the left operand and the Vector as the right operand. We might try this:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) 10.7 + vec2 print(vec2.comps)
We get the error: TypeError: unsupported operand type(s) for +: ‘float’ and ‘Vector’. The problem is that the __add__
function always treats its self
argument as the left operand of +, which is this case is a float, not a Vector. Thus the statement 10.7 + vec2
is calling the __add__
method from the float class, and not from the Vector class. The built-in __add__
in the float class does not, of course, know what a Vector is, so it doesn’t know what to do when you try to add a Vector to a float.
What we need, therefore, is some method similar to __add__
that will look at the right operand rather than the left operand to determine which class’s method to call. Fortunately, there is such a method, called __radd__
, where the extra ‘r’ stands for ‘right’. Here’s the new method:
class Vector(object): """a mathematical vector""" def __init__(self, comps): self.comps = comps def __add__(self, other): if isinstance(other, Vector): if len(other.comps) == len(self.comps): return Vector([self.comps[i] + other.comps[i] for i in range(len(self.comps))]) else: return None elif isinstance(other, (int, float)): self.comps += [other] return self else: return None def __radd__(self, other): if isinstance(other, (int, float)): self.comps.insert(0, other) return self
The __radd__
method is on lines 19 to 22. We need to be a bit careful in interpreting its arguments, as in this case, self
refers to the right operand (even though it comes first in the argument list) and other
refers to the left operand. Thus it is the right operand that calls the __radd__
method, with the left operand passed in as a parameter, which is just what we want. If we run this with the above code 10.7 + vec2
followed by print(vec2.comps)
, we find that vec2
is now [10.7, 5, 6, 7, 8], as required.
How does Python know when to call __add__
and when to call __radd__
? The rule is that if the right operand is a Vector but the left operand is not, then __radd__
is called. If the left operand is a Vector, then __add__
is called, no matter what type the right operand is. So in the above code, __add__
is called for the case where we add two Vectors and when we add a Vector and a number, with the Vector on the left.
In-place arithmetic operators
An in-place operator is one such as += which, for ints x and y and the expression x += y
adds y to x and makes x point to the new value. In fact, the __add__
method above also works for +=, with the left operand passed as self
and the right operand as other
. The following code illustrates how this works:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) vec1 += vec2 print(vec1.comps) print(vec2.comps)
vec1
now has comps of [6, 8, 10, 12] while vec2
remains unchanged.
What is effectively happening when the statement vec1 += vec2
is run is that the statement is rewritten as vec1 = vec1 + vec2
, and then the overloaded __add__
is called to evaluate the right hand side, with the result then assigned to the left hand side, which is vec1
here.
Note that the statement vec1 += vec2
does not return anything, so you can’t say something like vec3 = vec1 += vec2
. The value returned by the addition is assigned to vec1
.
There is a specialized method called __iadd__
which can be overloaded for an in-place addition, but for most practical purposes, an ordinary __add__
method will suffice.
Arithmetic operators and their methods
Here’s a table showing the most common arithmetic methods and their corresponding methods that can be overridden:
Addition | x + y | x.__add__(y) |
Subtraction | x - y | x.__sub__(y) |
Multiplication | x * y | x.__mul__(y) |
Division | x / y | x.__truediv__(y) |
Floor (integer) division | x // y | x.__floordiv__(y) |
Power | x ** y | x.__pow__(y) |
Modulo (remainder) | x % y | x.__mod__(y) |
All these methods work if the left operand is an object from the class in which the method is defined. If you want to have the right operand call the method, prefix the name with ‘r’, as above.
Exercise
Extend the Vector class above by writing overloaded operators for multiplication, using the * operator.
If both operands are Vectors, the * operator should return the scalar product of the two Vectors. This is defined only for Vectors of the same dimension. If we have two vectors and
, each with
components, then the scalar product
is defined as
That is, multiply together the corresponding entries from the two vectors and add up the result. Note that the result is just a number, not a vector.
If only one operand (either right or left) is a Vector and the other operand is a number, then the Vector should have all its components multiplied by the number. In this case, the result is a Vector, and should be a new Vector, so that the original Vector should be unchanged.
See answerThe complete Vector class including the multiplication overloads is:
class Vector(object): """a mathematical vector""" def __init__(self, comps): self.comps = comps def __add__(self, other): if isinstance(other, Vector): if len(other.comps) == len(self.comps): return Vector([self.comps[i] + other.comps[i] for i in range(len(self.comps))]) else: return None elif isinstance(other, (int, float)): self.comps += [other] return self else: return None def __radd__(self, other): if isinstance(other, (int, float)): self.comps.insert(0, other) return self def __mul__(self, other): if isinstance(other, Vector): if len(other.comps) == len(self.comps): scalar = 0 for i in range(len(self.comps)): scalar += self.comps[i] * other.comps[i] return scalar else: return None elif isinstance(other, (int, float)): return Vector([other * self.comps[i] for i in range(len(self.comps))]) else: return None def __rmul__(self, other): if isinstance(other, (int, float)): return self * other return None
The __mul__
method on line 24 handles the cases where both operands are Vectors and where the left operand is a Vector and the right is a number. Lines 26 to 30 check that the Vectors are the same length and then calculate and return the scalar product. Lines 33 to 35 create and return a new Vector with its comps
equal to the comps
of the self
Vector multiplied by other
.
To handle the case where the left operand is a number and the right operand is a Vector, we overload the __rmul__
method on line 39. Since this operation should give the same answer as the case where the Vector and number are swapped, we can just return self * other
, which, because the left operand is now a Vector, calls the overloaded __add__
method to calculate the result.
You can test the new methods with the code:
from Vector import Vector vec1 = Vector([1.2,2,3,4]) vec2 = Vector([5,6,7,8]) scalar = vec1 * vec2 print(scalar) vec6 = vec1 * 4.5 print(vec6.comps) vec7 = 9.4 * vec1 print(vec7.comps)