An abstract base class (ABC) is a class that cannot be used to create objects. Instead, the purpose of such classes is to define interfaces, that is, to in effect list the methods and properties that classes that inherit the abstract base class must provide. This is useful because we can use an abstract base class as a kind of promise—a promise that any derived class will provide the methods and properties that the abstract base class specifies.[*]
[*] Python’s abstract base classes are described in PEP 3119 (www.python.org/dev/peps/pep-3119), which also includes a very useful rationale and is well worth reading.
Abstract base classes are classes that have at least one abstract method or property. Abstract methods can be defined with no implementation (i.e., their suite is pass
, or if we want to force reimplementation in a subclass, raise NotImplementedError()
), or with an actual (concrete) implementation that can be invoked from subclasses, for example, when there is a common case. They can also have other concrete (i.e., nonabstract) methods and properties.
Classes that derive from an ABC can be used to create instances only if they reimplement all the abstract methods and abstract properties they have inherited. For those abstract methods that have concrete implementations (even if it is only pass
), the derived class could simply use super()
to use the ABC’s version. Any concrete methods or properties are available through inheritance as usual. All ABCs must have a metaclass of abc.ABCMeta
(from the abc
module), or from one of its subclasses. We cover metaclasses a bit further on.
Python provides two groups of abstract base classes, one in the collections
module and the other in the numbers
module. They allow us to ask questions about an object; for example, given a variable x
, we can see whether it is a sequence using isinstance(
x
, collections.MutableSequence)
or whether it is a whole number using isinstance(
x
, numbers.Integral)
. This is particularly useful in view of Python’s dynamic typing where we don’t necessarily know (or care) what an object’s type is, but want to know whether it supports the operations we want to apply to it. The numeric and collection ABCs are listed in Tables 3 and 4. The other major ABC is io.IOBase
from which all the file and stream-handling classes derive.
To fully integrate our own custom numeric and collection classes we ought to make them fit in with the standard ABCs. For example, the SortedList
class is a sequence, but as it stands, isinstance(
L
, collections.Sequence)
returns False
if L
is a SortedList
. One easy way to fix this is to inherit the relevant ABC:
class SortedList(collections.Sequence):
By making collections.Sequence
the base class, the isinstance()
test will now return True
. Furthermore, we will be required to implement __init__()
(or __new__()
), __getitem__()
, and __len__()
(which we do). The collections.Sequence
ABC also provides concrete (i.e., nonabstract) implementations for __contains__()
, __iter__()
, __reversed__()
, count()
, and index()
. In the case of SortedList
, we reimplement them all, but we could have used the ABC versions if we wanted to, simply by not reimplementing them. We cannot make SortedList
a subclass of collections.MutableSequence
even though the list is mutable because SortedList
does not have all the methods that a collections.MutableSequence
must provide, such as __setitem__()
and append()
. (The code for this SortedList
is in SortedListAbc.py
. We will see an alternative approach to making a SortedList
into a collections.Sequence
in the Metaclasses subsection.)
Now that we have seen how to make a custom class fit in with the standard ABCs, we will turn to another use of ABCs: to provide an interface promise for our own custom classes. We will look at three rather different examples to cover different aspects of creating and using ABCs.
We will start with a very simple example that shows how to handle read-able/writable properties. The class is used to represent domestic appliances. Every appliance that is created must have a read-only model string and a read-able/writable price. We also want to ensure that the ABC’s __init__()
is reimplemented. Here’s the ABC (from Appliance.py
); we have not shown the import abc
statement which is needed for the abstractmethod()
and abstractproperty()
functions, both of which can be used as decorators:
class Appliance(metaclass=abc.ABCMeta):
@abc.abstractmethod
def __init__(self, model, price):
self.__model = model
self.price = price
def get_price(self):
return self.__price
def set_price(self, price):
self.__price = price
price = abc.abstractproperty(get_price, set_price)
@property
def model(self):
return self.__model
We have set the class’s metaclass to be abc.ABCMeta
since this is a requirement for ABCs; any abc.ABCMeta
subclass can be used instead, of course. We have made __init__()
an abstract method to ensure that it is reimplemented, and we have also provided an implementation which we expect (but can’t force) inheritors to call. To make an abstract readable/writable property we cannot use decorator syntax; also we have not used private names for the getter and setter since doing so would be inconvenient for subclasses. The model
property is not abstract, so subclasses don’t need to reimplement it. No Appliance
objects can be created because the class contains abstract attributes. Here is an example subclass:
class Cooker(Appliance):
def __init__(self, model, price, fuel):
super().__init__(model, price)
self.fuel = fuel
price = property(lambda self: super().price,
lambda self, price: super().set_price(price))
The Cooker
class must reimplement the __init__()
method and the price
property. For the property we have just passed on all the work to the base class. The model
read-only property is inherited. We could create many more classes based on Appliance
, such as Fridge
, Toaster
, and so on.
The next ABC we will look at is even shorter; it is an ABC for text-filtering functors (in file TextFilter.py
):
class TextFilter(metaclass=abc.ABCMeta):
@abc.abstractproperty
def is_transformer(self):
raise NotImplementedError()
@abc.abstractmethod
def __call__(self):
raise NotImplementedError()
The TextFilter
ABC provides no functionality at all; it exists purely to define an interface, in this case an is_transformer
read-only property and a __call__()
method, that all its subclasses must provide. Since the abstract property and method have no implementations we don’t want subclasses to call them, so instead of using an innocuous pass
statement we raise an exception if they are used (e.g., via a super()
call).
Here is one simple subclass:
class CharCounter(TextFilter):
@property
def is_transformer(self):
return False
def __call__(self, text, chars):
count = 0
for c in text:
if c in chars:
count += 1
return count
This text filter is not a transformer because rather than transforming the text it is given, it simply returns a count of the specified characters that occur in the text. Here is an example of use:
Two other text filters are provided, both of which are transformers: RunLength-Encode
and RunLengthDecode
. Here is how they are used:
rle_encoder = RunLengthEncode()
rle_text = rle_encoder(text)
...
rle_decoder = RunLengthDecode()
original_text = rle_decoder(rle_text)
The run length encoder converts a string into UTF-8 encoded bytes, and replaces 0x00
bytes with the sequence 0x00
, 0x01
, 0x00
, and any sequence of three to 255 repeated bytes with the sequence 0x00
, count, byte. If the string has lots of runs of four or more identical consecutive characters this can produce a shorter byte string than the raw UTF-8 encoded bytes. The run length decoder takes a run length encoded byte string and returns the original string. Here is the start of the RunLengthDecode
class:
class RunLengthDecode(TextFilter):
@property
def is_transformer(self):
return True
def __call__(self, rle_bytes):
...
We have omitted the body of the __call__()
method, although it is in the source that accompanies this book. The RunLengthEncode
class has exactly the same structure.
The last ABC we will look at provides an Application Programming Interface (API) and a default implementation for an undo mechanism. Here is the complete ABC (from file Abstract.py
):
class Undo(metaclass=abc.ABCMeta):
@abc.abstractmethod
def __init__(self):
self.__undos = []
@abc.abstractproperty
def can_undo(self):
return bool(self.__undos)
@abc.abstractmethod
def undo(self):
assert self.__undos, "nothing left to undo"
self.__undos.pop()(self)
def add_undo(self, undo):
self.__undos.append(undo)
The __init__()
and undo()
methods must be reimplemented since they are both abstract; and so must the read-only can_undo
property. Subclasses don’t have to reimplement the add_undo()
method, although they are free to do so. The undo()
method is slightly subtle. The self.__undos
list is expected to hold object references to methods. Each method must cause the corresponding action to be undone if it is called—this will be clearer when we look at an Undo
subclass in a moment. So to perform an undo we pop the last undo method off the self.__undos
list, and then call the method as a function, passing self
as an argument. (We must pass self
because the method is being called as a function and not as a method.)
Here is the beginning of the Stack
class; it inherits Undo
, so any actions performed on it can be undone by calling Stack.undo()
with no arguments:
class Stack(Undo):
def __init__(self):
super().__init__()
self.__stack = []
@property
def can_undo(self):
return super().can_undo
def undo(self):
super().undo()
def push(self, item):
self.__stack.append(item)
self.add_undo(lambda self: self.__stack.pop())
def pop(self):
item = self.__stack.pop()
self.add_undo(lambda self: self.__stack.append(item))
return item
We have omitted Stack.top()
and Stack.__str__()
since neither adds anything new and neither interacts with the Undo
base class. For the can_undo
property and the undo()
method, we simply pass on the work to the base class. If these two were not abstract we would not need to reimplement them at all and the same effect would be achieved; but in this case we wanted to force subclasses to reimplement them to encourage undo to be taken account of in the subclass. For push()
and pop()
we perform the operation and also add a function to the undo list which will undo the operation that has just been performed.
Abstract base classes are most useful in large-scale programs, libraries, and application frameworks, where they can help ensure that irrespective of implementation details or author, classes can work cooperatively together because they provide the APIs that their ABCs specify.