Making your objects behave like built-in types
Print a plain object and Python shows something like <__main__.Position object at 0x7f3a>, and Position("A", 1) == Position("A", 1) is False because by default two objects are equal only if they are the very same object. Special methods (also called dunder methods, for the double underscores) let your class plug into Python's own syntax: __repr__ controls how an object is shown, __eq__ controls ==, __add__ controls +, __lt__ controls < and so on. You never call them directly; Python calls them when you use repr(x), a == b, a + b or sorted(items).
class Plain:
def __init__(self, cents):
self.cents = cents
a, b = Plain(500), Plain(500)
print(a == b) # False: different objects
print(a.cents == b.cents) # True
print("object at 0x" in str(a)) # True: the default display is unhelpful__repr__: show the object clearly
__repr__ returns a string that describes the object for developers. By convention it looks like the code that would build it: Money(1999, 'USD'). Lists, the debugger and error messages all use it, which makes bugs much easier to read. print(x) and str(x) use __repr__ too unless you also define __str__ for a prettier, user-facing text. It must return the string, not print it, and write {self.currency!r} in an f-string to get the quoted form of a string attribute.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
price = Money(1999)
print(price) # Money(1999, 'USD')
print([Money(5), Money(7, "EUR")]) # [Money(5, 'USD'), Money(7, 'EUR')]__eq__ and __hash__: what does equal mean?
__eq__(self, other) decides self == other. Compare the fields that define identity, and when other is not the type you expect, return the special value NotImplemented (not False): Python then tries the other side and finally answers False itself, so Money(1) == 5 is safe. Defining __eq__ switches off hashing, so objects can no longer be set members or dictionary keys unless you also define __hash__ consistently, for example hash((self.cents, self.currency)).
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python decide
return self.cents == other.cents and self.currency == other.currency
def __hash__(self):
return hash((self.cents, self.currency))
print(Money(500) == Money(500)) # True
print(Money(500) == Money(500, "EUR")) # False
print(Money(500) == 500) # False, no error
print(len({Money(1), Money(1)})) # 1: usable in sets nowOperators and ordering
__add__(self, other) gives your class +. Return a new object rather than changing self, and refuse nonsense: adding dollars to euros should raise ValueError, adding a bare number should return NotImplemented. __lt__(self, other) gives <, and that alone is enough for sorted(), min() and max() to order your objects, because they only need to ask "is a smaller than b?". Add __le__, __gt__, __ge__ too, or let functools.total_ordering derive them.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("cannot add different currencies")
return Money(self.cents + other.cents, self.currency)
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.cents < other.cents
total = Money(150) + Money(275)
print(total) # Money(425, 'USD')
print(sorted([Money(30), Money(10), Money(20)])) # [Money(10, 'USD'), Money(20, 'USD'), Money(30, 'USD')]
print(sum([Money(1), Money(2)], Money(0))) # Money(3, 'USD')__repr__ should show everything __eq__ looks at. Do not give + surprising side effects: it should return a new value and leave both operands untouched, like numbers do. The lab tests check these points: that originals are unchanged, mismatched currencies are refused, and unrelated types give NotImplemented.__repr__returns a string (likeMoney(1999, 'USD')); it does not print.__eq__compares fields and returnsNotImplementedfor foreign types; add__hash__to keep hashing.__add__returns a new object;__lt__is enough forsorted,minandmax.
Next up: dataclasses, which write __init__, __repr__ and __eq__ for you.
Lab · what's graded
- Show a Money object clearly with
__repr__25 pts - Say when two amounts are equal with
__eq__25 pts - Add two amounts with
__add__25 pts - Make amounts sortable with
__lt__25 pts