Execute Program

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, so Point(3, 4) * 2 gives us Point(6, 8). When we do x * y, Python calls .__mul__, so we can implement our multiplication there.

  • >
    class Point:
    def __init__(self, x, y):
    self.x = x
    self.y = y

    def __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)Pass Icon
  • 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" * 3
    Result:
    '222'Pass Icon
  • To fix the problem, we can add an isinstance check to ensure that other is an integer or float. If it's not, we'll refuse to multiply by returning NotImplemented, the same special value that we used when customizing equality operators.

  • >
    class Point:
    def __init__(self, x, y):
    self.x = x
    self.y = y

    def __mul__(self, other):
    if not isinstance(other, (int, float)):
    return NotImplemented
    return 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)Pass Icon
  • 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'Pass Icon
  • It's easy to make subtle mistakes when customizing mathematical operators. For example, we saw that we can successfully multiply Point by 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)Pass Icon
  • 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'Pass Icon
  • 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 + 1
    Result:
    TruePass Icon
  • >
    "a" + "b" == "b" + "a"
    Result:
    FalsePass Icon
  • >
    [1] + [2] == [2] + [1]
    Result:
    FalsePass Icon
  • To correctly implement a mathematical operator like *, we have to consider both sides. .__mul__ customizes what happens when the class is multiplied by the other value (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 that self and other are flipped. In 2 * Point(3, 4), self is the point and other is the integer 2.

  • Here's a new version of Point that works on either side of *.

  • >
    class Point:
    def __init__(self, x, y):
    self.x = x
    self.y = y

    def __mul__(self, other):
    if not isinstance(other, (int, float)):
    return NotImplemented
    return 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)Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    b = 2 * Point(3, 4)
    (b.x, b.y)
    Result:
    (6, 8)Pass Icon
  • Fortunately, implementing .__rmul__ was easy, since multiplying 2D points by integers is commutative, just like multiplying two numbers is commutative. 2 * 5 == 5 * 2, and Point(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 / b is not equal to b / a, and [1] + [2] is not equal to [2] + [1]!

  • The other mathematical operators follow the same pattern that we just saw for *:

    • a + b calls .__add__ and .__radd__
    • a - b calls .__sub__ and .__rsub__
    • a / b calls .__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 // b calls .__floordiv__ and .__rfloordiv__. (Remember that 1 // 2 is "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 Point class. For example, Point(2, 3) + 5 should return Point(7, 8). Implement that behavior by defining .__add__ and .__radd__.

    Remember that .__add__ should return NotImplemented when it doesn't know how to add to another value.

    A hint: .__radd__ can simply return self + other, which will call .__add__(other).

    class Point:
    def __init__(self, x, y):
    self.x = x
    self.y = y

    def __add__(self, other):
    if not isinstance(other, int):
    return NotImplemented
    return Point(self.x + other, self.y + other)

    def __radd__(self, other):
    return self + other
    a = Point(10, 20) + 5
    assert (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 + other

    def __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.Pass Icon