Chapter 13. Abstract Base Classes

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.

Table 3. The Numbers Module’s Abstract Base Classes

image

Table 4. The Collections Module’s Main Abstract Base Classes

image

image

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:

vowel_counter = CharCounter()

vowel_counter("dog fish and cat fish", "aeiou")     # returns: 5


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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset