Polymorphism and protocols¶
~20 min ยท prerequisite: Inheritance and super
What you will be able to do¶
- Write one loop that calls the same method on objects of different classes.
- Define an abstract base class with
abcthat requires its subclasses to provide a method. - Describe what an object must support with a
typing.Protocol, and check it withisinstance.
The idea¶
Polymorphism means "many forms." In programming, it means that one name can work on objects of different classes, and each object does the right thing for its own class. A drawing loop can call draw() on rectangles and circles alike, and each shape draws itself.
Python relies on duck typing, a term from the Python glossary. If an object has the method you call, you can use it, whatever its class is. A function that calls .area() on its input works with any object that has an area method.
Sometimes you want Python to check that a class provides a method. An abstract base class, or ABC, does this. You create one by inheriting from ABC in the abc module and marking required methods with @abstractmethod. Python then refuses to create an object from a subclass that does not provide every abstract method.
A protocol describes the methods an object must have, without requiring inheritance. You write a class that inherits from typing.Protocol and lists the method signatures. Any class with matching methods satisfies it. Add @runtime_checkable if you want isinstance to test for it. That check only confirms that the method names exist.
Use an ABC when related classes should share a required structure, and a protocol when unrelated classes should support the same operations.
Worked example¶
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Return the area of the shape."""
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
shapes = [Rectangle(2, 3), Triangle(3, 4)]
total = 0
for shape in shapes:
total += shape.area()
print(total)
The output is 12.0. Line by line, this is what Python does:
class Shape(ABC):creates an abstract base class. The@abstractmethodline marksareaas required for every subclass.RectangleandTriangleeach provide their ownarea, so both can be created.- In the loop,
shape.area()runs the method that belongs to each object's class. The results are6and6.0. totaladds them, andprintshows12.0, because a float appears in the sum.
Try it¶
The first block uses an ABC and tries to create the base class. The second uses a protocol that no class inherits from. The third shows duck typing in a function.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
shapes = [Rectangle(2, 3), Triangle(3, 4)]
total = 0
for shape in shapes:
total += shape.area()
assert total == 12.0
blocked = False
try:
Shape()
except TypeError:
blocked = True
assert blocked
from typing import Protocol, runtime_checkable
@runtime_checkable
class HasArea(Protocol):
def area(self) -> float: ...
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
class Label:
def __init__(self, text):
self.text = text
assert isinstance(Square(2), HasArea)
assert not isinstance(Label("x"), HasArea)
assert Square(3).area() == 9
Common mistakes¶
-
Creating the abstract base class directly. Writing
Shape()gives:TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'Create a subclass that defines
area, such asRectangle. -
Forgetting a required method in a subclass. A subclass of
Shapethat does not defineareagives the same error, with the subclass name in the message. -
Calling a method that the object does not have. Passing a number to
announcefrom the third block gives: -
Using
isinstancewith a protocol that is not runtime-checkable. Without the@runtime_checkableline,isinstance(1, HasArea)gives:Add the decorator above the class.
Practice¶
- Create
Square(Shape)with an__init__that takesside, and anareamethod. Assert thatSquare(3).area()is9. - Write a function
total_area(shapes)that returns the sum ofarea()for each item in a list. Assert thattotal_area([Square(2), Square(1)])is5, using theSquareyou wrote in exercise 1. - Create a protocol
Namedwith a methodname(self) -> str. Create a classCatwith anamemethod that returns"cat", and assert thatisinstance(Cat(), Named)isTrue. Then assert thatisinstance(5, Named)isFalse. - Create two classes,
DogandRobot, each with aspeakmethod. Write a loop that collects the result ofspeak()for each object in a list, and assert that the list is["Woof", "Beep"]. - Stretch: create an abstract base class
Storagewith abstract methodssave(self, item)andload(self). CreateListStorage(Storage)that keeps items in a list. Assert that saving"a"and"b"and then callingload()returns["a", "b"], and thatStorage()raisesTypeError.
Solution
- ```python from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): pass
class Square(Shape): def init(self, side): self.side = side
def area(self):
return self.side * self.side
assert Square(3).area() == 9 ```
-
```python class Square: def init(self, side): self.side = side
def area(self): return self.side * self.side
def total_area(shapes): total = 0 for shape in shapes: total += shape.area() return total
assert total_area([Square(2), Square(1)]) == 5 ```
- ```python from typing import Protocol, runtime_checkable
@runtime_checkable class Named(Protocol): def name(self) -> str: ...
class Cat: def name(self): return "cat"
assert isinstance(Cat(), Named) assert not isinstance(5, Named) ```
- ```python class Dog: def speak(self): return "Woof"
class Robot: def speak(self): return "Beep"
results = [] for thing in [Dog(), Robot()]: results.append(thing.speak()) assert results == ["Woof", "Beep"] ```
- ```python from abc import ABC, abstractmethod
class Storage(ABC): @abstractmethod def save(self, item): pass
@abstractmethod
def load(self):
pass
class ListStorage(Storage): def init(self): self.items = []
def save(self, item):
self.items.append(item)
def load(self):
return self.items
store = ListStorage() store.save("a") store.save("b") assert store.load() == ["a", "b"]
blocked = False try: Storage() except TypeError: blocked = True assert blocked ```
Recap¶
- Polymorphism lets one method name work on objects of different classes, each running its own version.
- An abstract base class (
ABCwith@abstractmethod) stops you from creating a subclass that leaves out a required method. - A
Protocoldescribes required methods, and@runtime_checkableletsisinstancecheck for them.