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