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
Withint
?⟶
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 aboutint
Compares directly
Number(2) == 2
True
Less straightforward: compare int
with Number
int.__eq__
knows nothingReturns
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 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
@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 < r
⇔r > 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 |
|
|
Subtraction |
|
|
Multiplication |
|
|
Power |
|
|
Division |
|
|
Floor Division |
|
|
Modulo |
|
|
Bitwise Left Shift |
|
|
Bitwise Right Shift |
|
|
Bitwise AND |
|
|
Bitwise OR |
|
|
Bitwise XOR |
|
|