Classes and objectsChapter 65 of 114
Polymorphism
Different types answering the same call, and why Python barely notices.
The same call, different objects
Polymorphism means code that works with any object providing the operation it needs:
class Dog:
def speak(self):
return "woof"
class Cat:
def speak(self):
return "meow"
class Robot:
def speak(self):
return "beep"
for thing in [Dog(), Cat(), Robot()]:
print(thing.speak())Output
woof meow beep
Note Robot is not related to the others at all. It just has speak, and that is enough.
Duck typing
The rule is: if it walks like a duck and quacks like a duck, treat it as a duck. Python checks whether the operation works, not what the type is:
def describe(items):
return f"{len(items)} items, first is {items[0]}"
print(describe([1, 2, 3]))
print(describe("abc"))
print(describe((True, False)))Output
3 items, first is 1 3 items, first is a 2 items, first is True
One function, three unrelated types, no inheritance anywhere.
Builtins are polymorphic too
len(), +, in and for all work on anything that implements the right method, including yours:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
p = Playlist(["a", "b", "c"])
print(len(p))
print(p[1])
print([song for song in p])Output
3 b ['a', 'b', 'c']
Defining __getitem__ was enough to make the object loopable. That is polymorphism in the other direction: your class fitting into Python's rules.
Checking capability, not type
Asking whether something can do the job is more flexible than asking what it is:
def make_speak(thing):
if hasattr(thing, "speak"):
return thing.speak()
return "cannot speak"
class Dog:
def speak(self):
return "woof"
print(make_speak(Dog()))
print(make_speak(42))Output
woof cannot speak
Often the cleanest version does neither, and simply tries:
def make_speak(thing):
try:
return thing.speak()
except AttributeError:
return "cannot speak"
print(make_speak(42))Output
cannot speak
Abstract base classes
When you do want to insist on an interface, abc refuses to build a subclass that has not implemented it:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(3).area())
class Blob(Shape):
pass
try:
Blob()
except TypeError as problem:
print("TypeError:", problem)Output
9 TypeError: Can't instantiate abstract class Blob without an implementation for abstract method 'area'
The error arrives when someone tries to build the incomplete class, rather than much later when something calls the missing method.
Operator overloading
Define the dunder and the operator works:
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __str__(self):
return f"{self.amount:.2f}"
print(Money(4.5) + Money(2.25))Output
6.75
Use it where the meaning is obvious. + on two amounts of money reads well; + meaning "send an email" does not.
Test yourself
2 questionsWhat is duck typing?
Show the answer
Caring whether an object supports the operation, not what type it is — It is why a function can accept a list, a string and a tuple with no shared base class.
What does an abstract base class give you?
Show the answer
An error when someone builds a subclass that has not implemented a required method — The error arrives at construction rather than much later, when something calls the missing method.
Iterators
How for loops actually work, and how to make your own object loopable.