Arithmetic overloading

Arithmetic overloading#

Python allows for some very fancy overloading of just about every operation. We’ll briefly show you how to do overloading for numeric operations.

Suppose we want to design a class that lets us work with three-dimensional vectors. There are a number of operations we might want to define:

  • vector addition (add the components together)

  • scalar addition (add a value to every component)

  • scalar multiplication (multiply every component by a value)

  • equality (return True on equal vectors)

Python lets us define these operations so that we can use existing Python + and * and == operators. Let’s see how. Here’s the code:

We put some print statements at overloaded methods to clarify what’s getting called when. Here’s an example interaction:

We put some print statements at overloaded methods to clarify what’s getting called when. Here’s an example interaction:

>>> origin = Vector(0, 0, 0)
>>> v1 = Vector(1, 1, 1)
>>> x = Vector(1, 0, 0)
>>> origin + 1
Vector.__add__((0, 0, 0), 1)
Vector(1, 1, 1)
>>> 1 + origin
Vector.__radd__((0, 0, 0), 1)
Vector(1, 1, 1)
>>> origin + 1 == v1
Vector.__add__((0, 0, 0), 1)
Vector.__eq__((1, 1, 1), (1, 1, 1))
True
>>> origin + v1
Vector.__add__((0, 0, 0), (1, 1, 1))
Vector(1, 1, 1)
>>> v1 + origin
Vector.__add__((1, 1, 1), (0, 0, 0))
Vector(1, 1, 1)
>>> x * 3
Vector.__mul__((1, 0, 0), 3)
Vector(3, 0, 0)
>>> x * 3 + v1
Vector.__mul__((1, 0, 0), 3)
Vector.__add__((3, 0, 0), (1, 1, 1))
Vector(4, 1, 1)

From the sample interactions, we can see that __add__ gets called on the object on the left and __radd__ gets called on the object on the right. But why does the call to __radd__ happen when we write 1 + origin? The key is in the NotImplemented return value.

Overloaded operations tend to take the form of:

  • Test isinstance against the current class, and then return the right answer.

  • Test isinstance against other types that could work, and then return the right answer.

  • Give up and return NotImplemented (or raise NotImplementedError).

Given e1 + e2, Python will first try calling e1.__add__(e2). If that doesn’t work (e.g., NotImplemented), then it will try e2.__radd__(e1). Since int.__add__ doesn’t know about Vector, it gave back a NotImplemented, so 1 + origin called origin.__radd__(1).

We also overloaded __eq__… and __hash__. Overloading __eq__ lets us define a notion of equality—very convenient! It is very important that if you overload __eq__, you overload __hash__. The __hash__ function takes an arbitrary value and produces a number. If two values are __eq__ to each other (i.e., they are equal according to ==), then they must have the same hash. Values with the same hash need not be equal, though.

You’ll learn more about hashing in a data structures course. For now, it suffices to take any relevant state/members and hash the tuple of them, as our __hash__ here does.