Interfaces and Abstract Classes
Poll: Which of these would make a good superclass / subclass pair?
- Rectangle / Square
- Sophomore / Freshman
- Mammal / Elephant
- Building / Window
Abstract methods
An abstract method is a method with no implementation.
class Shape():
def get_area(self) -> float:
pass
def get_perimeter(self) -> float:
pass
The Shape
class above has two abstract methods get_area()
and get_perimeter()
. Since we don't know enough about the type of shape, we don't have enough information to implement these two methods. Instead, we implement these methods in all of Shape
's subclasses:
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def get_area(self) -> float:
return self.width * self.height
def get_perimeter(self) -> float:
return 2 * (self.width + self.height)
This is nice because we were able to define what a generic Shape
does, but leave the implementation details to its subclasses, who have the necessary information.
rect: Shape = Rectangle(5, 3)
print(rect.get_area()) # 15
The only thing is, the above design allows us to instantiate a general Shape
and ask for its area or perimeter -- how embarassing! We don't have the answers for those things!
shape = Shape()
print(shape.get_area()) # None
And the same thing will happen if we forget to implement one of the abstract methods in one of Shape
's subclasses.
The ABC
module in Python helps us with that: it prevents us from instantiating a class that has an abstract method (either inherited from a superclass or directly written in the class).
We can use the ABC
module to block anyone from instantiating Shape
because it has an abstract method:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def get_area(self) -> float:
pass
@abstractmethod
def get_perimeter(self) -> float:
pass
shape = Shape() # TypeError
Every method decorated with @abstractmethod
must be implemented by a subclass to get instantiated. If a subclass doesn't implement all abstract methods, the ABC
module will raise a TypeError
when you try to instantiate it.
The Rectangle
class is unchanged and works the same as before, because it had already implemented all necessary methods.
https://giffiles.alphacoders.com/207/207370.gif
Poll: Which ones are legal?
- pet1: Pet = Pet()
- cat1: Cat = Cat()
- dog1: Dog = Dog()
- pet2: Pet = Cat()
- cat2: Cat = Dog()
- dog2: Dog = Pet()
Poll: Does this work?
for pet in [Cat(), Dog(), Cat()]:
pet.express_affection()
- Yes
- No
- I don't know
- I looked ahead in the online lecture notes and found the answer
class Pet(ABC):
@abstractmethod
def express_affection(self) -> None:
pass
class Cat(Pet):
def express_affection(self) -> None:
self.make_biscuits()
def make_biscuits(self) -> None:
print('Making biscuits')
class Dog(Pet):
def express_affection(self) -> None:
self.slobber()
def slobber(self) -> None:
print('Slobbering')
for pet in [Cat(), Dog(), Cat()]:
pet.express_affection()
Making biscuits
Slobbering
Making biscuits
Interfaces
An interface describes the behavior of a class without implementing it. You may have heard of a user interface: it also describes the behavior of the application without telling you how that behavior is implemented.
An interface is a contract: if a class wants to "implement" the interface, that class must implement each specified method.
- Different classes can implement the same methods in different ways
- Implementing classes can also have additional methods not specified in the interface
In Python, interfaces are written as abstract classes (ABC
module) where all of the methods in it are abstract.
Poll: (Designing an interface) What should all classes which implement the interface Cat
be able to do?
- Sleep
- Roar
- Meow
- Bark
- Knead
Poll: Which types can be instantiated and put into the list cacophony
?
cacophony: List[Roarable] = list()
class Cat(ABC):
pass
class Roarable(ABC):
@abstractmethod
def roar(self) -> None:
pass
class Lion(Cat, Roarable):
def roar(self) -> None:
print('ROAR')
class AsiaticLion(Lion):
pass
class HouseCat(Cat):
pass
class Dragon(Roarable):
def roar(self) -> None:
print('GRRRR')
- Lion
- AsiaticLion
- HouseCat
- Dragon
- Roarable
Interfaces with duck typing in Python
The ABC
(Abstract Base Classes) module was originally designed to help with abstraction: inheritance hierarchies (subclass / superclass relationships) where we just happen to need abstract methods which are implemented in subclasses.
Using the ABC
module to design interfaces is commonplace in modern Python, but some argue that that is not what it was originally designed for. Interfaces are "contracts" that specify what a class should be able to do. Abstract classes are classes that happen to need abstract methods because they are too non-specific, but those methods can be implemented in their more specific subclasses. Interfaces serve different purposes than abstract superclasses, and the ABC
module was designed for the latter, not the former.
One can argue that interfaces are not necessary in a language like Python, which uses "duck typing." You may have heard of the Duck Test: "If it walks like a duck and it quacks like a duck, then it must be a duck." In Python, where the types are not enforced by default, we can pass a variable of any type to a function expecting any type. If the variable happens to have the necessary methods and attributes to work in that context (to quack), great! It's a duck.
https://stackoverflow.com/questions/4205130/what-is-duck-typing
Interfaces can make less sense in a language that uses duck typing because they force types to follow contracts for what they should be able to do, and then pass variables of those types into locations where type flexibility is prioritized.
There is tension in the community about this. We find interfaces to be a valuable concept for this course because:
- It helps us to detect errors early, not while running the program
- We prioritize readability, and making contracts explicit through interfaces helps with "self-documentation"
- It helps us to keep track of types' capabilities, especially in large codebases
- When designing APIs for others to use, it helps ensure that implementers implement all required methods
- It prepares students for future courses where types and interfaces are fundamental concepts
Python contracts
An interface using ABC
is an explicit contract: classes must follow the rules in order to be used in a specific way.
Python also has built-in contracts which are followed by convention but not enforced.
There are pros and cons of using both.
Explicit contracts through ABC
interfaces are enforced, which is beneficial for the reasons listed above (early error detection, readability, easier to follow, requires other implementors to follow our rules, teaches fundamental concepts).
Implicit contracts, on the other hand, can include things that interfaces cannot include. These contracts can say things about what the methods should do, rather than simply listing the methods that need to be implemented. Implicit contracts can require specific method behaviors because they are not enforced by Python.
collections.abc.Sized for len()
We are able to get the sizes of many different objects using the same len()
function. That's because, in Python, there is a built-in contract called the "length protocol" or "size protocol." It says that we should write a method called def __len__(self) -> int
which returns a non-negative int
, and this is what is returned by the len()
function.
class Cat:
def __len__(self) -> int:
return 900
print(len(Cat())) # 900
There is an interface in ABC
which enforces this protocol:
from collections.abc import Sized
class Cat(Sized):
def __len__(self) -> int:
return 900
print(len(Cat())) # 900
Neglecting to implement the __len__()
function (or having it return a negative number) will result in an error before even being run.
collections.abc.Container for in
Similarly, we can use the in
operator because of the implicit Python contract called the "membership test protocol" or "containment protocol": when you use in
, Python calls __contains__()
.
The protocol works on its own, but we often enforce it using collections.abc.Container
:
from collections.abc import Container
class Document(Container[str]):
def __init__(self, text: str):
self.words = text.split()
def __contains__(self, word: object) -> bool:
if not isinstance(word, str):
raise TypeError
return word in self.words
print('hi' in Document('hi this is mini')) # True
print('cat' in Document('hi this is mini')) # False