Difficulty: Intermediate
What are dunder (magic) methods? How do they enable operator overloading?
Dunder (double underscore) methods are special methods that Python calls implicitly to perform operations on objects. They enable operator overloading and protocol implementation.
Common categories: - Construction: __init__, __new__, __del__ - String representation: __str__ (user-friendly), __repr__ (developer-friendly) - Comparison: __eq__, __lt__, __le__, __gt__, __ge__, __ne__ - Arithmetic: __add__, __sub__, __mul__, __truediv__ - Container: __len__, __getitem__, __setitem__, __contains__ - Context: __enter__, __exit__ - Callable: __call__
from functools import total_ordering
@total_ordering # Generates __le__, __gt__, __ge__ from __eq__ and __lt__
class Money:
def __init__(self, amount, currency='USD'):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
def __str__(self):
return f"${self.amount:.2f} {self.currency}"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError("Cannot compare different currencies")
return self.amount < other.amount
def __hash__(self):
return hash((self.amount, self.currency))
m1 = Money(10.50)
m2 = Money(20.00)
print(str(m1)) # '$10.50 USD'
print(repr(m2)) # "Money(20.0, 'USD')"
print(m1 < m2) # True
print(m1 >= m2) # False (from @total_ordering)
__str__ is for end users (print), __repr__ is for developers (debug). Return NotImplemented (not raise) to let Python try the reflected operation.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar):
return self.__mul__(scalar) # 3 * v == v * 3
def __abs__(self):
return (self.x 2 + self.y 2) 0.5
def __bool__(self):
return self.x != 0 or self.y != 0
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(9, 12)
print(3 * v1) # Vector(9, 12) - uses __rmul__
print(abs(v1)) # 5.0
print(bool(Vector(0, 0))) # False
__rmul__ handles the case where the left operand doesn't support the operation (3 * v). __abs__ is called by abs(), __bool__ by bool() and in if statements.
class Playlist:
def __init__(self, name, songs=None):
self.name = name
self._songs = songs or []
def __len__(self):
return len(self._songs)
def __getitem__(self, index):
return self._songs[index] # Supports indexing AND slicing
def __setitem__(self, index, value):
self._songs[index] = value
def __contains__(self, song):
return song in self._songs
def __iter__(self):
return iter(self._songs)
def __repr__(self):
return f"Playlist('{self.name}', {len(self)} songs)"
pl = Playlist("Road Trip", ["Song A", "Song B", "Song C", "Song D"])
print(len(pl)) # 4
print(pl[0]) # 'Song A'
print(pl[-1]) # 'Song D'
print(pl[1:3]) # ['Song B', 'Song C']
print("Song B" in pl) # True
for song in pl:
print(song, end=', ') # Song A, Song B, Song C, Song D,
Implementing __len__ and __getitem__ makes your class work with len(), indexing, slicing, and for loops. __contains__ enables the 'in' operator.
__init__, __str__, __repr__, __eq__, __lt__, __add__, __len__, __getitem__