Operator Overloading

Attention

Respect!

  • Colleagues want to understand your code

  • Only overload operators when it makes sense

  • It is hard to get right!

  • Lots of magic behind the scene (reverse operations, proper use of NotImplemented)

Operators Are “Dunder” Methods

Pythonicity: even the innocent int type is a class

int
int

… with all consequences: class dictionary, methods (err, operators)

int.__dict__['__add__']
<slot wrapper '__add__' of 'int' objects>

Operators …

1 + 2
3

… are methods:

int.__add__(1, 2)
3

Hypothetical And Pointless class Number

class Number:
    def __init__(self, n):
        self.n = n

Simplest: Equality Comparison (==)

  • By default, Python defines object equality as object identity

  • ⟶ two different objects with the same value compare un-equal

l = Number(2)
r = Number(2)

l==r      # <--- same as "l is r"
False
  • Not whats expected

  • ⟶ overload == by defining the __eq__ method

class Number:
    def __init__(self, n):
        self.n = n
    def __eq__(self, other):
        return self.n == other.n
l = Number(2)
r = Number(2)

l == r
True
  • Number does not define __ne__

  • Special Python behaivor

  • Python calls __eq__ and negates the result

l != r    # <--- not (l==r)
False

Comparing Against Incompatible Types? (Lotsa Magic!)

  • Number With int?

  • NotImplemented triggers more special Python behavior

import numbers

class Number:
    def __init__(self, n):
        self.n = n
    def __eq__(self, other):
        if isinstance(other, Number):           # <--- easy
            return self.n == other.n
        elif isinstance(other, numbers.Number): # <--- any Python number
            return self.n == other
        else:
            return NotImplemented               # <--- magic

Straightforward: compare Number with int

  • Number.__eq__ knows about int

  • Compares directly

Number(2) == 2
True

Less straightforward: compare int with Number

  • int.__eq__ knows nothing

  • Returns NotImplemented

i = 2
i == Number(2)
True

But, at a lower level …

i.__eq__(Number(2))
NotImplemented
  • As a fallback of that, Python reverses operands, thereby asking the right hand operand

two = Number(2)
two.__eq__(2)
True
  • Et voila …

i = 2
i == Number(2)
True

Special behavior: this is the same as …

number2 = Number(2)
int2 = 2
result = int2.__eq__(number2)
if result is NotImplemented:    # <--- retry, in reverse order
    result = number2.__eq__(int2)

result
True

Not so special: comparing with e.g. str

  • Both direct and reversed operation return NotImplemented

Number(2) == '2'
False

Ordering: Less-Than (<) Operator

As opposed to equality comparison (==, !=), Python itself does not order objects:

Number(2) < Number(3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 Number(2) < Number(3)

TypeError: '<' not supported between instances of 'Number' and 'Number'

Straightforward implementation; no different from __eq__:

import numbers

class Number:
    def __init__(self, n):
        self.n = n
    def __lt__(self, other):
        if isinstance(other, Number):
            return self.n < other.n
        elif isinstance(other, numbers.Number):
            return self.n < other
        else:
            return NotImplemented

Solves problem:

Number(2) < Number(3)
True

Ordering Magic, Again: __gt__ in terms of __lt__

Magically, we see > implemented:

l = Number(3)
r = Number(2)

l > r
True

No surprise though; Python knows that this is the same as r < l:

hasattr(l, '__gt__')
True

Table: Comparison Operators

Operator

Method

<

__lt__

<=

__le__

==

__eq__

!=

__ne__

>

__gt__

>=

__ge__

@functools.total_ordering To The Rescue

  • Even without the reflection magic, implementing all of these is much writing

  • @functools.total_ordering decorator

import numbers
import functools

@functools.total_ordering
class Number:
    def __init__(self, n):
        self.n = n
    def __eq__(self, other):
        if isinstance(other, Number):
            return self.n == other.n
        elif isinstance(other, numbers.Number):
            return self.n == other
        else:
            return NotImplemented
    def __lt__(self, other):
        if isinstance(other, Number):
            return self.n < other.n
        elif isinstance(other, numbers.Number):
            return self.n < other
        else:
            return NotImplemented

Number(1) <= Number(2)
# ... and all the other operators ...
True

Arithmetic Operators

Reverting back to start …

class Number:
    def __init__(self, n):
        self.n = n

Number(1) - Number(2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[24], line 5
      2     def __init__(self, n):
      3         self.n = n
----> 5 Number(1) - Number(2)

TypeError: unsupported operand type(s) for -: 'Number' and 'Number'
  • No surprise: implement __sub__

  • Ideally, with full-bloated NotImplemented bells and whistles

import numbers

class Number:
    def __init__(self, n):
        self.n = n
    def __sub__(self, other):
        if isinstance(other, Number):
            return Number(self.n - other.n)
        elif isinstance(other, numbers.Number):
            return Number(self.n - other)
        else:
            return NotImplemented

sum = Number(1) - 2
sum.n
-1

Arithmetic Operators, Reverse Operations

  • Comparison operators are easy: operations are symmetric (l < rr > l)

  • Arithmetic operators are not generally symmetric/commutative: l - r vs. r - l

  • NotImplemented fallbacks handled differently

l = 1
r = Number(2)

l - r
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[26], line 4
      1 l = 1
      2 r = Number(2)
----> 4 l - r

TypeError: unsupported operand type(s) for -: 'int' and 'Number'

Hm. int knows nothing:

l.__sub__(r)
NotImplemented
  • Python knows that arithmetic operator are not generally symmetric

  • Does not call r.__sub__(l) as a fallback

  • __rsub__ - reverse subtraction

import numbers

class Number:
    def __init__(self, n):
        self.n = n
    def __sub__(self, other):
        if isinstance(other, Number):
            return Number(self.n - other.n)
        elif isinstance(other, numbers.Number):
            return Number(self.n - other)
        else:
            return NotImplemented
    def __rsub__(self, other):
        if isinstance(other, Number):
            return Number(other.n - self.n)
        elif isinstance(other, numbers.Number):
            return Number(other - self.n)
        else:
            return NotImplemented

l = 1
r = Number(2)

diff = l - r
diff.n
-1

Table: Operators And The Methods To Implement Them

Here’s a table summarizing the rest of the operators that can be overloaded in Python. Reverse operations are not listed; they are usually prefixed with an r, as in __rsub__, __radd__, __rrshift__.

Meaning

Operator

Method

Addition

+

__add__

Subtraction

-

__sub__

Multiplication

*

__mul__

Power

**

__pow__

Division

/

__truediv__

Floor Division

//

__floordiv__

Modulo

%

__mod__

Bitwise Left Shift

<<

__lshift__

Bitwise Right Shift

>>

__rshift__

Bitwise AND

&

__and__

Bitwise OR

|

__or__

Bitwise XOR

^

__xor__