Classes and objectsChapter 69 of 114
Magic Methods
The dunder methods that make your class work with Python's own syntax.
Python calls them for you
Methods with two underscores either side are how the language talks to your object. You never call __len__ directly; you call len() and Python calls it:
class Basket:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
basket = Basket(["apple", "pear"])
print(len(basket))
print(bool(basket), bool(Basket([])))Output
2 True False
bool() falls back to __len__ when there is no __bool__, so emptiness works for free.
Printing
class Money:
def __init__(self, amount):
self.amount = amount
def __str__(self):
return f"{self.amount:.2f} EUR"
def __repr__(self):
return f"Money({self.amount!r})"
price = Money(4.5)
print(price)
print(repr(price))
print([price])Output
4.50 EUR Money(4.5) [Money(4.5)]
Containers use __repr__, which is why a class with only __str__ still shows addresses inside a list.
Equality, and hashing with it
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
a = Point(1, 2)
print(a == Point(1, 2))
print(len({Point(1, 2), Point(1, 2)}))Output
True 1
Comparison and sorting
Define __lt__ and sorted() works. functools.total_ordering fills in the rest from that plus __eq__:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.pair = (major, minor)
def __eq__(self, other):
return self.pair == other.pair
def __lt__(self, other):
return self.pair < other.pair
def __repr__(self):
return f"v{self.pair[0]}.{self.pair[1]}"
versions = [Version(1, 4), Version(1, 2), Version(2, 0)]
print(sorted(versions))
print(Version(1, 2) <= Version(1, 4))
print(max(versions))Output
[v1.2, v1.4, v2.0] True v2.0
Arithmetic
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __mul__(self, count):
return Money(self.amount * count)
def __str__(self):
return f"{self.amount:.2f}"
print(Money(4.5) + Money(2.25))
print(Money(4.5) * 2)Output
6.75 9.00
Overload where the meaning is obvious. + on two amounts of money reads well; + meaning "send an email" does not.
Indexing, containment, iteration
class Playlist:
def __init__(self, songs):
self.songs = songs
def __getitem__(self, index):
return self.songs[index]
def __contains__(self, song):
return song in self.songs
def __iter__(self):
return iter(self.songs)
p = Playlist(["a", "b", "c"])
print(p[1])
print(p[0:2])
print("b" in p)
print([song for song in p])Output
b ['a', 'b'] True ['a', 'b', 'c']
Slicing came free, because __getitem__ passes the slice straight to the list.
Context managers
__enter__ and __exit__ make your object usable with with:
class Section:
def __init__(self, name):
self.name = name
def __enter__(self):
print("start", self.name)
return self
def __exit__(self, exc_type, exc, tb):
print("end", self.name)
return False
with Section("build"):
print("working")Output
start build working end build
__exit__ runs even when the block raises. Returning False lets the exception continue on its way, which is nearly always what you want.
Callable objects
class Adder:
def __init__(self, amount):
self.amount = amount
def __call__(self, value):
return value + self.amount
add_five = Adder(5)
print(add_five(10))
print(callable(add_five))Output
15 True
The ones worth knowing
| Method | Called by |
|---|---|
__init__ | building the object |
__repr__ / __str__ | repr() / print() |
__len__ | len(), and bool() as a fallback |
__eq__ / __hash__ | ==, sets, dict keys |
__lt__ | <, sorted(), max() |
__getitem__ | obj[i], and slicing |
__contains__ | in |
__iter__ | for, list() |
__call__ | obj() |
__enter__ / __exit__ | with |
Test yourself
2 questionsWhat happens to your class in a set if you define __eq__ but not __hash__?
Show the answer
It stops working, because __hash__ becomes None — Define both, and make them agree: objects that compare equal must hash equal.
What does returning True from __exit__ do?
Show the answer
Swallows the exception raised in the block — Almost always wrong. Return False, or nothing, so the exception continues on its way.
Modules
Split code across files, and pull in what the standard library already has.