C.W.K.
Stream
Lesson 04 of 07 · published

Dunder Methods Part 2 — Operator Overloading and Container Behavior

~22 min · dunder, operator-overloading, len, getitem, iter, contains

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Operator overloading — what each symbol calls

Every operator in Python maps to a dunder. + is __add__. - is __sub__. * is __mul__. < is __lt__. The full list is in the data model. Implementing them lets you write v1 + v2 on your custom Vector class. Reflected versions (__radd__, etc.) handle the case where your class is on the right side of an operator with a different type on the left.

Container dunders — len, getitem, contains, iter

__len__ makes len(obj) work. __getitem__ makes obj[i] work. __contains__ makes x in obj work. __iter__ makes for x in obj work. If you implement __getitem__ with integer indices, you get free iteration too — Python falls back to __getitem__(0), __getitem__(1), ... until IndexError.

__bool__ — truthiness for your objects

Without __bool__, Python uses __len__ if defined (length 0 is falsy) or treats every instance as truthy. Define __bool__ when neither default is right for your type. The method must return a bool.

__call__ — making your instance act like a function

Define __call__ and you can write my_instance(args). Used everywhere — class-based decorators, function objects with state, callable strategies. We saw it in the decorators track.

Principle: Operator overloading is great when the operations have a natural meaning for your type (Vector + Vector, Money + Money). It's bad when the operation is forced or ambiguous (User + User?). The bias: if a reader can't predict what a + b does just from knowing the types, don't overload.

Code

Vector with operator overloading·python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

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

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    __rmul__ = __mul__       # support 3 * v as well as v * 3

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)          # Vector(4, 6)
print(v2 - v1)          # Vector(2, 2)
print(v1 * 3)           # Vector(3, 6)
print(2 * v2)           # Vector(6, 8) — thanks to __rmul__
Container dunders — make your class iterable·python
class Deck:
    def __init__(self, cards):
        self._cards = list(cards)

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, i):
        return self._cards[i]

    def __contains__(self, card):
        return card in self._cards

# Now Deck behaves like a sequence
d = Deck(["A♥", "K♦", "Q♣", "J♠"])
print(len(d))            # 4
print(d[0])              # 'A♥'
print(d[-1])             # 'J♠'
print("K♦" in d)         # True

# Free iteration — __getitem__ with ints gets us this
for card in d:
    print(card)
Comparison dunders — sorting your objects·python
from functools import total_ordering

@total_ordering              # gives you all 6 comparison ops from __eq__ and __lt__
class Score:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if not isinstance(other, Score):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other):
        if not isinstance(other, Score):
            return NotImplemented
        return self.value < other.value

    def __repr__(self):
        return f"Score({self.value})"

scores = [Score(85), Score(92), Score(70), Score(100)]
print(sorted(scores))         # sorted by value
print(max(scores))            # Score(100)
print(Score(50) < Score(60))  # True
__bool__ and __call__·python
class Inbox:
    def __init__(self):
        self.messages = []

    def __bool__(self):
        return len(self.messages) > 0

    def __call__(self, msg):
        self.messages.append(msg)

inbox = Inbox()
if inbox:
    print("have messages")
else:
    print("empty")           # this prints

inbox("hello")              # __call__ — adds a message
inbox("world")

if inbox:
    print(f"have {len(inbox.messages)} messages")

External links

Exercise

Build a class Polynomial representing a polynomial as a list of coefficients (constant term first). Implement __repr__ (e.g. 'Polynomial([1, 0, 2])' meaning 1 + 0x + 2x²), __add__ (returns a new Polynomial with coefficients added pairwise; pad shorter with zeros), __call__(x) to evaluate the polynomial at x. Test addition and evaluation: (Polynomial([1, 2, 3]) + Polynomial([0, 1]))(2) should be 1 + 3*2 + 3*4 = 19.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.