Snippets Python ABC

abc - Abstract Base Classes

By Marcelo Fernandes Jul 13, 2017

Basic Acknowledges:

As stated on the references, "Abstract base classes' real power lies in the way they allow you to customise the behaviour of isinstance and issubclass"

A good example to get started, is by looking at the source code of python standard libraries. Let's check the collections.Container class:


class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Note: __dict__ Holds all the methods that describes the object and 
__mro__ is just a tuple of that holds: the class, its base, its base's base and so on... up to object
Example: class C(A, B): pass C.__mro__ # (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)
Therefore, this definition of __subclasshook__ says that any class with a __contains__ in its methods, or in its parents methods, it can be considered a subclass of Container, even if it does not subclass it directly.

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

This resumes to the conclusion that: If you implement the right interface, you are a subclass.
ABCs will provide a formal way to define interfaces in Python. The use of this class generally comes during refactoring when one or more functions are making the same attribute checks.

Consequences

A handy feature of ABCs is that if you do not implement all necessary methods that it requires (and properties) you will get an error upon instantiation, rather than an AttributeError, potentially much later during the runtime.

from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # Oh, we forgot to declare bar()

>>> c = Concrete()
# TypeError: Can't instantiate abstract class Concrete with abstract methods bar

Note: The TypeError informs which methods are missing, which is pretty good info.

Advantages:

1) It will make much easier determining whether an object supports a given protocol without having to check for the presence of all methods in the protocol, or without triggering an exception deep in "enemy" territory due to non-support.

2) It makes more difficult to write invalid subclasses. It might not be a big deal writing new code, but a few weeks or months down the line it will be very helpful.

Notes