Python in Detail: Customizing Mathematical Operators
Welcome to the Customizing Mathematical Operators lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In a previous lesson, we defined the
.__eq__dunder method to customize the==operator. We can also customize various mathematical operators.Suppose that we want the
Point(x, y)class to support multiplication, soPoint(3, 4) * 2gives usPoint(6, 8). When we dox * y, Python calls.__mul__, so we can implement our multiplication there.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __mul__(self, other):return Point(self.x * other, self.y * other)- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
a = Point(3, 4)b = a * 2(b.x, b.y)Result:
(6, 8)
That works, but there's a problem: we don't gracefully handle type mismatches. For example, multiplying a point by a string doesn't make sense, so we would expect to get an error. Unfortunately, we don't get an error at all; Python happily returns a value!
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
a = Point(3, 4)b = a * "2"(b.x, b.y)Result:
That looks strange, but Python did exactly what we asked it to do. Strings support the
*operator, which duplicates the string.>
"2" * 3Result:
'222'
To fix the problem, we can add an
isinstancecheck to ensure thatotheris an integer or float. If it's not, we'll refuse to multiply by returningNotImplemented, the same special value that we used when customizing equality operators.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __mul__(self, other):if not isinstance(other, (int, float)):return NotImplementedreturn Point(self.x * other, self.y * other)Now points can only be multiplied by integers and floats. Anything else is an error.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
a = Point(6, 4)b = a * 0.5(b.x, b.y)Result:
(3.0, 2.0)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
b = Point(3, 4) * "2"(b.x, b.y)Result:
TypeError: can't multiply sequence by non-int of type 'Point'
It's easy to make subtle mistakes when customizing mathematical operators. For example, we saw that we can successfully multiply
Pointby a number. But if we flip the two and multiply a number by a point, we get an error!- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
b = Point(3, 4) * 2(b.x, b.y)Result:
(6, 8)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
# Note that we defined .__mul__, but not .__rmul__.b = 2 * Point(3, 4)(b.x, b.y)Result:
TypeError: unsupported operand type(s) for *: 'int' and 'Point'
Why does one order succeed and the other order error? While
2 * 3 == 3 * 2, Python knows there are some data types where that's not true.>
1 + 2 == 2 + 1Result:
True
>
"a" + "b" == "b" + "a"Result:
False
>
[1] + [2] == [2] + [1]Result:
False
To correctly implement a mathematical operator like
*, we have to consider both sides..__mul__customizes what happens when the class is multiplied by theothervalue (self * other). If we want to allow some value to be multiplied by the class (other * self), we need to define an additional dunder method:.__rmul__("right multiplication")..__rmul__is just like.__mul__, except thatselfandotherare flipped. In2 * Point(3, 4),selfis the point andotheris the integer 2.Here's a new version of
Pointthat works on either side of*.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __mul__(self, other):if not isinstance(other, (int, float)):return NotImplementedreturn Point(self.x * other, self.y * other)def __rmul__(self, other):return self * other- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
b = Point(3, 4) * 2(b.x, b.y)Result:
(6, 8)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
b = 2 * Point(3, 4)(b.x, b.y)Result:
(6, 8)
Fortunately, implementing
.__rmul__was easy, since multiplying 2D points by integers is commutative, just like multiplying two numbers is commutative.2 * 5 == 5 * 2, andPoint(x, y) * z == z * Point(x, y).Not all operations are commutative, so we sometimes need different implementations for
.__mul__and.__rmul__. For example:a / bis not equal tob / a, and[1] + [2]is not equal to[2] + [1]!The other mathematical operators follow the same pattern that we just saw for
*:a + bcalls.__add__and.__radd__a - bcalls.__sub__and.__rsub__a / bcalls.__truediv__and.__rtruediv__. (Older versions of python used the less surprising.__div__and.__rdiv__, but it was replaced by "truediv" for reasons that are out of scope for this course.)a // bcalls.__floordiv__and.__rfloordiv__. (Remember that1 // 2is "floor division", which always returns an integer.)
Several other less-common operators also follow this pattern. For example,
a << b(the binary left shift operator) calls.__lshift__and the unfortunately-named.__rlshift__.A final note about custom operators. We could customize them to do things that aren't math. Even with built-in operations, the fact that
[1] + [2]is[1, 2]instead of[3]is a choice motivated by programmer convenience.It's best to customize mathematical operators sparingly. When we do customize them, it's best to stick to behaviors that other programmers will expect. For example, multiplying a point by a number has a pretty obvious result: it multiplies the point's components by the number.
Some languages customize operators for more creative purposes. For example, in C++, we can print with code like
cout << "You have " << 3 << " apples", which prints"You have 3 apples". This kind of creative operator overloading is rarely used in Python, so other Python programmers won't expect it.Here's a code problem:
We want to be able to add integers to the
Pointclass. For example,Point(2, 3) + 5should returnPoint(7, 8). Implement that behavior by defining.__add__and.__radd__.Remember that
.__add__should returnNotImplementedwhen it doesn't know how to add to another value.A hint:
.__radd__can simply returnself + other, which will call.__add__(other).class Point:def __init__(self, x, y):self.x = xself.y = ydef __add__(self, other):if not isinstance(other, int):return NotImplementedreturn Point(self.x + other, self.y + other)def __radd__(self, other):return self + othera = Point(10, 20) + 5assert (a.x, a.y) == (15, 25)b = 6 + Point(11, 21)assert (b.x, b.y) == (17, 27)# The Point class should refuse to do additions that don't make sense.assert_raises(TypeError, lambda: Point(1, 2) + "a")# This class is a helper for the test below.class AdditionTestHelper:def __add__(self, other):return self + otherdef __radd__(self, other):return [other]# If Point doesn't know how to add to a value, it should use# NotImplemented to defer to the other operand in the + operation. We# use an AdditionTestHelper instance to check that. When "adding", it# wraps the added value in a list. The fact that it's a list isn't# important; it's just an easy way for us to check that the addition- Goal:
No errors.
- Yours:
No errors.