ABCs
In my last post I gave an example of a class you’d only inherit and not directly instantiate and so it seemed worthwhile to write a bit about abtract base classes too.
The typical form you encounter is when someone wants to ensure certain methods are implemented. They’ll inherit from ABC (or specify metaclass=abc.ABCMeta
, which results in the same thing) then specify an abstractmethod
which needs to be implemented. For example we have a class A
below which is meant to be inherited.
1
2
3
4
5
6
7
8
import abc
class A(metaclass=abc.ABCMeta):
@abc.abstractmethod
def foo(self):
pass
a = A()
-> If we try to instantiate A itself we get TypeError: Can't instantiate abstract class A with abstract method foo
. The following will result in a similar error
1
2
3
4
class B(A):
pass
b = B()
However, once you’ve specified the methods you’re good to go and the following will work.
1
2
3
4
5
class B(A):
def foo(self):
return True
b = B()
Though if you want to corrupt A
you can of course.. and so the following will work too :)
1
2
3
4
5
class B(A):
pass
B.__abstractmethods__ = frozenset()
b = B()
But that isn’t really the point of my example or I think the main great thing about abstract base classes.
One of the main reasons they’re interesting is because an implementation of a class can be considered to be a subclass or an instance of an abstract class.. even when you haven’t inherited / specified or done anything at all to it! The actual PEP back in 2007 for this - PEP-3119, goes into some details.
A great result of this PEP was the ability to recognise an object (such as a class) conforms to a particular protocol. i.e. That the class has certain kinds of behavior and that it can do certain methods. The collections module is probably the best example of these (source code here).
ABCs implement the dunders expected but most importantly the __subclasshook__
which checks the class method resolution order and dict for the expected methods. Realistically what this means is that I can create a list and validate it’s an Iterable.
1
2
3
4
5
6
from collections.abc import Iterable
a_list = [1,2,3]
print(isinstance(a_list, Iterable)) # True
print(issubclass(a_list, Iterable)) # Errors because it's not a class
It also means I can make my custom class - Say, an Iterator which requires the __iter__
and __next__
methods implemented, then actually validate it in my tests using a simple isinstance
or issubclass
. In some ways this makes testing easier and understanding what I expect from my class a whole lot more comprehensible.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections.abc import Iterable, Iterator
class A:
def __init__(self) -> None:
self.a = 1
def __iter__(self):
return self
def __next__(self):
self.a = self.a + self.a
return self.a
print(isinstance(A, Iterable)) # False
print(isinstance(A(), Iterable)) # True
print(issubclass(A, Iterable)) # True
print(isinstance(A, Iterator)) # False
print(isinstance(A(), Iterator)) # True
print(issubclass(A, Iterator)) # True
a = A()
print(next(a)) # 2
print(next(a)) # 4
print(next(a)) # 8
print(next(a)) # 16