Dunder methods¶
~20 min ยท prerequisite: Polymorphism and protocols
What you will be able to do¶
- Make your own class work with operators such as
+,*, and==by writing dunder methods. - Give an object a readable text form with
__repr__and use it withprint,repr, andstr. - Use
len,in, andsortedwith your own classes, and know when a class can be used in a set.
The idea¶
A dunder method is a method whose name starts and ends with two underscores, such as __add__. ("Dunder" is short for "double underscore.") Python calls these methods when you use an operator or built-in function on your object. When you write v1 + v2, Python calls v1.__add__(v2). When you write len(store), Python calls store.__len__().
__add__ handles +, and __mul__ handles *. __eq__ handles ==, and __lt__ handles <. __len__ handles len, and __contains__ handles in. __repr__ gives the official text form of an object, precise enough to rebuild it, such as Vector(1, 2). print and str use __str__ when a class defines it, and otherwise fall back to __repr__.
Defining __eq__ also sets __hash__ to None, so the object can no longer be a set element or dictionary key. To allow that, define __hash__ too, so that equal objects give equal hash values.
The standard library's dataclasses module can write __init__, __repr__, and __eq__ for simple classes. This lesson writes them by hand. See the dataclasses documentation for the shortcut.
Worked example¶
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)
print(v1 * 3)
print(v1 == Vector(1, 2))
The output is Vector(4, 6), then Vector(3, 6), then True. Line by line, this is what Python does:
__add__runs when Python sees+between two vectors, and__mul__runs for*with a number on the right. Each returns a newVector.__eq__runs for==, and returnsTrueonly when both parts match.__repr__builds the textVector(x, y), whichprintuses here.v1 + v2callsv1.__add__(v2), givingVector(4, 6).v1 * 3callsv1.__mul__(3), givingVector(3, 6).v1 == Vector(1, 2)calls__eq__, and the parts match, so the result isTrue.
Try it¶
The first block tests the operators and text form, the second adds len and in to an inventory, and the third makes objects sortable.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
assert v1 + v2 == Vector(4, 6)
assert repr(v1 * 2) == "Vector(2, 4)"
assert v1 != v2
assert str(v1) == "Vector(1, 2)"
class Inventory:
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
def __len__(self):
return len(self.items)
def __contains__(self, name):
return name in self.items
store = Inventory()
store.add("pen")
store.add("mug")
assert len(store) == 2
assert "pen" in store
assert "book" not in store
Common mistakes¶
-
Adding a number to a vector. Writing
Vector(1, 2) + 5calls__add__withotherset to5. Inside it,other.xfails: -
Misspelling the initializer name. Writing
def _init_(self, x, y):with one underscore on each side means Python never calls it. ThenVector(1, 2)gives: -
Forgetting to return a value. If
__add__creates a vector but does notreturnit, the result ofv1 + v2isNone. Then(v1 + v2).xgives:Every operator method must return the result.
-
Putting an object with
__eq__into a set. Defining__eq__without__hash__makes the object unhashable. Writing{Vector(1, 2)}gives:Define
__hash__as well if the object must go in a set. -
Expecting
==to compare contents without__eq__. A class with no__eq__compares identity, soassert Vector(1, 2) == Vector(1, 2)fails with:
Practice¶
- Give a class
Pricea__repr__that returnsPrice(5)forPrice(5). Assert thatrepr(Price(5))equals"Price(5)". - Create a class
Shelfwith anitemslist, anadd(self, name)method, and a__len__method. Add two names and assert thatlen(shelf)is2. - Add a
__sub__method toVectorthat subtracts part by part. Assert thatVector(5, 5) - Vector(1, 2)equalsVector(4, 3). - Add a
__str__method toVectorthat returns"(1, 2)"style text. Assert thatstr(Vector(1, 2))is"(1, 2)", and thatrepr(Vector(1, 2))is still"Vector(1, 2)". - Stretch: create
Moneywithamount, an__add__that returns a newMoney, an__eq__, and a__hash__that returnshash(self.amount). Assert that(Money(2) + Money(3)).amountis5, and thatlen({Money(1), Money(1)})is1.
Solution
-
```python class Price: def init(self, amount): self.amount = amount
def repr(self): return f"Price({self.amount})"
assert repr(Price(5)) == "Price(5)" ```
-
```python class Shelf: def init(self): self.items = []
def add(self, name): self.items.append(name)
def len(self): return len(self.items)
shelf = Shelf() shelf.add("pen") shelf.add("mug") assert len(shelf) == 2 ```
-
```python class Vector: def init(self, x, y): self.x = x self.y = y
def sub(self, other): return Vector(self.x - other.x, self.y - other.y)
def eq(self, other): return self.x == other.x and self.y == other.y
def repr(self): return f"Vector({self.x}, {self.y})"
assert Vector(5, 5) - Vector(1, 2) == Vector(4, 3) ```
-
```python class Vector: def init(self, x, y): self.x = x self.y = y
def str(self): return f"({self.x}, {self.y})"
def repr(self): return f"Vector({self.x}, {self.y})"
assert str(Vector(1, 2)) == "(1, 2)" assert repr(Vector(1, 2)) == "Vector(1, 2)" ```
-
```python class Money: def init(self, amount): self.amount = amount
def add(self, other): return Money(self.amount + other.amount)
def eq(self, other): return self.amount == other.amount
def hash(self): return hash(self.amount)
assert (Money(2) + Money(3)).amount == 5 assert len({Money(1), Money(1)}) == 1 ```
Recap¶
- A dunder method such as
__add__,__eq__, or__len__runs when Python sees the matching operator or built-in function on your object. __repr__gives the text form of an object, andprintuses it when no__str__is defined.- Defining
__eq__makes the object unhashable unless you also define__hash__.