indusai.co

Special methods

Special methods have double underscores on both sides of their name, which is why they are called dunder methods. Python calls them for you in response to built in operations: printing, adding, comparing, indexing, iterating. Defining them makes your own classes behave like built in types.

str and repr

__str__ is the friendly version shown by print(). __repr__ is the developer version shown in the shell and inside collections, and should ideally look like the code that would recreate the object.

python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

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

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

p = Point(2, 3)
print(p)
print([p, Point(0, 0)])

If you only define one, define __repr__; print falls back to it.

eq and ordering

By default two objects are equal only if they are the same object. Define __eq__ to compare by value.

python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    def __lt__(self, other):
        return (self.x, self.y) < (other.x, other.y)

print(Point(1, 2) == Point(1, 2))
print(sorted([Point(3, 1), Point(1, 5)], key=lambda p: (p.x, p.y))[0].x)

With __lt__ defined you can sort a list of points directly. functools.total_ordering fills in the remaining comparison methods from __eq__ and one other.

Arithmetic operators

python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(1, 2) + Vector(3, 4)
print(v)
print(v * 3)
OperatorMethod
+__add__
-__sub__
*__mul__
/__truediv__
//__floordiv__
%__mod__
**__pow__
==, !=__eq__, __ne__
<, <=, >, >=__lt__, __le__, __gt__, __ge__

len, getitem and contains

These make an object work with len(), indexing and in.

python
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)
    def __len__(self):
        return len(self.songs)
    def __getitem__(self, i):
        return self.songs[i]
    def __contains__(self, song):
        return song in self.songs

pl = Playlist(["Intro", "Verse", "Chorus"])
print(len(pl))
print(pl[1])
print("Chorus" in pl)
for s in pl:          # __getitem__ alone is enough to make it iterable
    print(s)

call

Makes an instance callable like a function.

python
class Multiplier:
    def __init__(self, k):
        self.k = k
    def __call__(self, x):
        return x * self.k

triple = Multiplier(3)
print(triple(10))

enter and exit

These power the with statement. See the file handling lesson for the usual case, but you can write your own:

python
class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self
    def __exit__(self, exc_type, exc, tb):
        import time
        print(f"took {time.perf_counter() - self.start:.4f}s")

with Timer():
    sum(range(100000))

bool and hash

__bool__ decides truthiness. __hash__ is needed if objects with __eq__ are used as dictionary keys or set members. Dataclasses can generate both, which is usually the easiest route.

Practice

  1. Give the Vector class __sub__ and __eq__, then check that Vector(3, 3) - Vector(1, 1) == Vector(2, 2).
  2. Write a Deck class that holds cards and supports len(deck), deck[0] and for card in deck.
  3. Write a Money class whose __str__ prints an amount like Rs 1,250.00.