6.6. Operators

6.6.1. Rationale

  • Operator Overload

  • Readable syntax

  • Simpler operations

  • Following examples uses dataclasses to focus on action code, not boilerplate

from dataclasses import dataclass

@dataclass
class Vector:
    x: int = 0
    y: int = 0


Vector(x=1, y=2) + Vector(x=3, y=4)
# Traceback (most recent call last):
# TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'

Vector(x=1, y=2) + Vector(x=3, y=4) + Vector(x=5, y=6)
# Traceback (most recent call last):
# TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'
from dataclasses import dataclass

@dataclass
class Vector:
    x: int = 0
    y: int = 0

    def __add__(self, other):
        return Vector(
            self.x + other.x,
            self.y + other.y)


Vector(x=1, y=2) + Vector(x=3, y=4)
# Vector(x=4, y=6)

Vector(x=1, y=2) + Vector(x=3, y=4) + Vector(x=5, y=6)
# Vector(x=9, y=12)

6.6.2. Numerical Operators

Table 6.2. Numerical Operator Overload

Operator

Method

obj + other

obj.__add__(other)

obj - other

obj.__sub__(other)

obj * other

obj.__mul__(other)

obj / other

obj.__truediv__(other)

obj // other

obj.__floordiv__(other)

obj ** other

obj.__pow__(other)

obj % other

obj.__mod__(other)

obj @ other

obj.__matmul__(other)

obj += other

obj.__iadd__(other)

obj -= other

obj.__isub__(other)

obj *= other

obj.__imul__(other)

obj /= other

obj.__idiv__(other)

obj //= other

obj.__itruediv__(other)

obj **= other

obj.__ipow__(other)

obj %= other

obj.__imod__(other)

obj @= other

obj.__imatmul__(other)

% (__mod__) operator behavior for int and str:

class int:
    def __mod__(self, other):
        """modulo division"""


class str:
    def __mod__(self, other):
        """str substitute"""

        if type(other) is str:
            ...
        if type(other) is tuple:
            ...
        if type(other) is dict:
            ...


3 % 2                         # 1
4 % 2                         # 0

'Echo' % 2                    # TypeError: not all arguments converted during string formatting
'Echo %s' % 2                 # 'Echo 2'
'Echo %d' % 2                 # 'Echo 2'
'Echo %f' % 2                 # 'Echo 2.0'
'Echo %s %s' % (1, 2)         # 'Echo 1 2'
'Echo %s %d %f' % (1, 2, 3)   # 'Echo 1 2 3.000000'

'Echo %(firstname)s %(lastname)s' % {'firstname': 'Mark', 'lastname': 'Watney'}
# 'Echo Mark Watney'

'Echo %(name)s %(age)d' % {'name': 'Mark Watney', 'age': 44}
# 'Echo Mark Watney 44'

%s, %d, %f is currently deprecated in favor of f'...' string formatting. More information in Builtin Printing.

6.6.3. Comparison Operators

Table 6.3. Comparison Operators Overload

Operator

Method

obj == other

obj.__eq__(other)

obj != other

obj.__ne__(other)

obj < other

obj.__lt__(other)

obj <= other

obj.__le__(other)

obj > other

obj.__gt__(other)

obj >= other

obj.__ge__(other)

from dataclasses import dataclass

@dataclass
class Vector:
    x: int = 0
    y: int = 0

    def __eq__(self, other):
        if (self.x == other.x) and (self.y == other.y):
            return True
        else:
            return False

Vector(x=1, y=2) == Vector(x=3, y=4)
# False

Vector(x=1, y=2) == Vector(x=1, y=2)
# True

6.6.4. Boolean Operators

Table 6.4. Boolean Operators Overload

Operator

Method

-obj

obj.__neg__()

+obj

obj.__pos__()

~obj

obj.__invert__()

obj & other

obj.__and__(other)

obj | other

obj.__or__(other)

obj ^ other

obj.__xor__(other)

obj << other

obj.__lshift__(other)

obj >> other

obj.__rshift__(other)

obj &= other

obj.__iand__(other)

obj |= other

obj.__ior__(other)

obj ^= other

obj.__ixor__(other)

obj <<= other

obj.__ilshift__(other)

obj >>= other

obj.__irshift__(other)

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0
1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
from dataclasses import dataclass

@dataclass
class Digit:
    value: int

    def __xor__(self, other):
        return Digit(self.value ** other.value)


a = Digit(2)
b = Digit(4)

a ^ b
# Digit(value=16)

6.6.5. Builtin Functions and Keywords

Table 6.5. Builtin Functions Overload

Function

Method

abs(obj)

obj.__abs__()

bool(obj)

obj.__bool__()

complex(obj)

obj.__complex__()

del obj

obj.__del__()

delattr(obj, name)

obj.__delattr__(name)

dir(obj)

obj.__dir__()

divmod(obj, other)

obj.__divmod__(other)

float(obj)

obj.__float__()

getattr(obj, name, default)

obj.__getattr__(name, default)

hash(obj)

obj.__hash__()

hex(obj)

obj.__hex__()

int(obj)

obj.__int__()

iter(obj)

obj.__iter__()

len(obj)

obj.__len__()

next(obj)

obj.__next__()

oct(obj)

obj.__oct__()

pow(obj)

obj.__pow__()

reversed(obj)

obj.__reversed__()

round(obj, ndigits)

obj.__round__(ndigits)

setattr(obj, name)

obj.__setattr__(name)

from math import sqrt
from dataclasses import dataclass

@dataclass
class Vector:
    x: int = 0
    y: int = 0

    def __abs__(self):
        return sqrt(self.x**2 + self.y**2)


abs(Vector(x=3, y=4))
# 5.0
class Astronaut:
    def __float__(self) -> float:
        return 1961.0

    def __int__(self) -> int:
        return 1969

    def __len__(self) -> int:
        return 170

    def __str__(self) -> str:
        return 'My name... José Jiménez'

    def __repr__(self) -> str:
        return f'Astronaut()'

astro = Astronaut()

float(astro)
# 1961.0

int(astro)
# 1969

len(astro)
# 170

repr(astro)
# Astronaut()

str(astro)
# 'My name... José Jiménez'

print(astro)
# My name... José Jiménez

6.6.6. Accessors Overload

Table 6.6. Operator Overload

Operator

Method

Remarks

obj(x)

obj.__call__(x)

obj[x]

obj.__getitem__(x)

obj[x]

obj.__missing__(x)

(when x is not in obj)

obj[x] = 10

obj.__setitem__(x, 10)

del obj[x]

obj.__delitem__(x)

x in obj

obj.__contains__(x)

>>> data = slice(1, 2, 3)
>>>
>>> data.start
1
>>> data.stop
2
>>> data.step
3
class MyClass:
    def __getitem__(self, item):
        print(item)


my = MyClass()
my[1:2]
# slice(1, 2, None)
data = dict()

data['a'] = 10
# data.__setitem__('a', 10) -> None

data['a']
# data.__getitem__('a') -> 10

data['x']
# data.__getitem__('x') -> data.__missing__() -> KeyError: 'x'

data()
# data.__call__() -> TypeError: 'dict' object is not callable

Contains in numpy:

import numpy as np


data = np.array([[1, 2, 3],
                 [4, 5, 6]])

data[1][2]
# 6

data[1,2]
# 6

data[1:2]
# array([[2,3],
#        [5,6]])

data[1:2, 0]
# array([2,3])

data[1:2, 1:]
# array([[5,6]])

Intuitive implementation of numpy array[row,col] accessor:

class array(list):
    def __getitem__(key):
        if isinstance(key, int):
            return super().__getitem__(key)

        if isinstance(key, tuple):
            row = key[0]
            col = key[1]
            return super().__getitem__(row).__getitem__(col)

        if isinstance(key, slice):
            start = key[0] if key[0] else 0
            stop = key[1] if key[0] else len(self)
            step = key[2] if key[2] else 1
            return ...


data[1]
# data.__getitem__(1)

data[1,2]
# data.__getitem__((1,2))

data[1:2]
# data.__getitem__(1:2)
# data.__getitem__(slice(1,2))

data[:, 2]
# data.__getitem__((:, 2))
# data.__getitem__((slice(), 2))

6.6.7. Eq Works at Both Sides

class Astronaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname


class Cosmonaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname


a = Astronaut('Mark', 'Watney')
c = Cosmonaut('Mark', 'Watney')

print(a == c)
# False
class Astronaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

    def __eq__(self, other):
        return (self.firstname == other.firstname)
           and (self.lastname == other.lastname)


class Cosmonaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname


a = Astronaut('Mark', 'Watney')
c = Cosmonaut('Mark', 'Watney')

print(a == c)
# True
class Astronaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname


class Cosmonaut:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

    def __eq__(self, other):
        return (self.firstname == other.firstname)
           and (self.lastname == other.lastname)


a = Astronaut('Mark', 'Watney')
c = Cosmonaut('Mark', 'Watney')

print(a == c)
# True

6.6.8. Use Cases

hero @ Position(x=50, y=120)
hero >> Direction(left=10, up=20)
hero < Damage(20)
hero > Damage(20)
hero["gold"] += dragon["gold"]
class Cache(dict):
    def __init__(self, func):
        self._func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        self[key] = self._func(*key)
        return self[key]


@Cache
def myfunction(a, b):
    return a * b


myfunction(2, 4)           # 8         # Computed
myfunction('hi', 3)        # 'hihihi'  # Computed
myfunction('ha', 3)        # 'hahaha'  # Computed

myfunction('ha', 3)        # 'hahaha'  # Fetched from cache
myfunction('hi', 3)        # 'hihihi'  # Fetched from cache
myfunction(2, 4)           # 8         # Fetched from cache
myfunction(4, 2)           # 8         # Computed

myfunction
# {
#   (2, 4): 8,
#   ('hi ', 3): 'hihihi',
#   ('ha', 3): 'hahaha',
#   (4, 2): 8,
# }

6.6.10. Assignments

Code 6.6. Solution
"""
* Assignment: OOP Overload Matmul
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Use code from "Given" section (see below)
    2. Overload `@` operator
    3. Set position based on argument `tuple[int, int]`
    4. Run doctests - all must succeed

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Przeciąż operator `@`
    3. Ustaw pozycję na podstawie argumentu `tuple[int, int]`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> position = Position()
    >>> position
    Position(x=0, y=0)
    >>> position @ (1, 2)
    >>> position
    Position(x=1, y=2)
"""


# Given
from dataclasses import dataclass


@dataclass
class Position:
    x: int = 0
    y: int = 0


Code 6.7. Solution
"""
* Assignment: OOP Overload IAdd
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. Use code from "Given" section (see below)
    2. Override operator `+=` for code to work correctly
    3. Run doctests - all must succeed

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Nadpisz operatory `+=` aby poniższy kod zadziałał poprawnie
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `obj.__iadd__(other) -> self`

Tests:
    >>> astro = Astronaut('Jan Twardowski', missions=[
    ...     Mission(1969, 'Apollo 11'),
    ... ])
    >>> astro += Mission(2024, 'Artemis 3')
    >>> astro += Mission(2035, 'Ares 3')

    >>> print(astro)  # doctest: +NORMALIZE_WHITESPACE
    Astronaut(name='Jan Twardowski',
              missions=[Mission(year=1969, name='Apollo 11'),
                        Mission(year=2024, name='Artemis 3'),
                        Mission(year=2035, name='Ares 3')])
"""


# Given
from dataclasses import dataclass


@dataclass
class Astronaut:
    name: str
    missions: list


@dataclass
class Mission:
    year: int
    name: str


Code 6.8. Solution
"""
* Assignment: OOP Overload Equals
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. Use code from "Given" section (see below)
    2. Override operator for code to work correctly
    3. Do not use `dataclasses`
    4. Run doctests - all must succeed

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Nadpisz operator aby poniższy kod zadziałał poprawnie
    3. Nie używaj `dataclasses`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> Mission(2035, 'Ares 3') == Mission(2035, 'Ares 3')
    True
    >>> Mission(2035, 'Ares 3') == Mission(1973, 'Apollo 18')
    False
    >>> Mission(2035, 'Ares 3') == Mission(2035, 'Apollo 18')
    False
    >>> Mission(2035, 'Ares 3') == Mission(1973, 'Ares 3')
    False
"""


# Given
class Mission:
    def __init__(self, year, name):
        self.year = year
        self.name = name


Code 6.9. Solution
"""
* Assignment: OOP Overload Contains
* Complexity: easy
* Lines of code: 5 lines
* Time: 8 min

English:
    1. Use code from "Given" section (see below)
    2. Override operators for code to work correctly
    3. Do not use `dataclasses`
    4. Run doctests - all must succeed

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Nadpisz operatory aby poniższy kod zadziałał poprawnie
    3. Nie używaj `dataclasses`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> astro = Astronaut('Jan Twardowski', missions=[
    ...     Mission(1969, 'Apollo 11'),
    ...     Mission(2024, 'Artemis 3'),
    ...     Mission(2035, 'Ares 3'),
    ... ])

    >>> Mission(2035, 'Ares 3') == Mission(2035, 'Ares 3')
    True
    >>> Mission(2035, 'Ares 3') == Mission(1973, 'Apollo 18')
    False
    >>> Mission(2035, 'Ares 3') == Mission(2035, 'Apollo 18')
    False
    >>> Mission(2035, 'Ares 3') == Mission(1973, 'Ares 3')
    False

    >>> Mission(2024, 'Artemis 3') in astro
    True
    >>> Mission(1973, 'Apollo 18') in astro
    False
"""


# Given
class Astronaut:
    def __init__(self, name, missions):
        self.name = name
        self.missions = missions


class Mission:
    def __init__(self, year, name):
        self.year = year
        self.name = name