Abstract base classes and how to use them in your data science project | by Sebastian Dick | Towards Data Science

Have you ever encountered code like this and wondered what’s so abstract about these classes and methods? Well, I’m about to show you!
If you stick around long enough I will also share a little trick that you can use to automate unit testing across your classes.
One key concept of object-oriented programming (OOP) is inheritance. Inheritance means that we base one (child-)class on another (parent-)class. The child class thereby inherits certain properties from its parent while at the same time implementing new ones.
Let’s look at an example. Imagine you want to write a software library about the animal world. You would perhaps start by defining a class Animal:
class Animal:    def __init__(self, height, weight):        self.height = height        self.weight = weight
Animal is a very general term, which is why you would like to create more specific classes:
class Dog(Animal):         def make_sound(self):        print('woof!')class Cat(Animal):         def make_sound(self):        print('meow!')        def pounce(self):        print('Pouncing at prey!')
Writing Animal in parentheses indicates that Dog and Cat inherit from the Animal class. This means that, without having to write it out, both animals automatically implement the __init__ method. In addition, both child classes implement their own methods: “make_sound” and “pounce”. As a rule of thumb, inheritance makes sense if your two classes fit the sentence “A is a B”: “Dog is an Animal”.
Let’s say you want to implement a lion class. Lions are cats, so we would want the class to inherit from Cat. Doing so, the methods that Cat inherited from Animal carry over to Lion as well
class Lion(Cat):         def make_sound(self):        print('roar!')
The class Lion now has three methods: __init__, pounce and make_sound. By redefining make_sound in the Lion class we have overwritten the version inherited from Cat.
Abstract base classes (ABC) provide a way to regulate this inheritance and at the same time structure your code. An ABC can be seen as the scaffolding or template (not to be confused with actual templates, think: C++!) for all classes inheriting from it.
In Python, one can define an ABC by inheriting from abc.ABC:
import abc class Animal(abc.ABC):
An ABC is not meant to be instantiated, i.e. one should not (and in certain cases cannot) create objects of an ABC.
my_animal = Animal(height=100, weight=80) #Don't do this!
So, why bother using abstract base classes at all? Because, as I said before, they add structure and safety to your project. The ABC “Animal” tells future developers working on your code (or future you) how any class inheriting from Animal should operate. To do so, ABCs have a tool at their disposal:
Abstract Methods
An abstract method is a method that has to be implemented by any class inheriting from the ABC.
We could, e.g. decide that every animal in our code has to be able to make some kind of sound. We can then define:
from abc import ABC, abstractmethodclass Animal(ABC):    def __init__(self, height, weight):        self.height = height        self.weight = weight    @abstractmethod    def make_sound(self):        passclass Bird(Animal):    def fly(self):        print('I am flying')
If we try to instantiate any child-class that does not implement the abstract method, we get the following error:
tweetie = Bird(height=5, weight=1)>>> TypeError: Can't instantiate abstract class Bird with abstract methods make_sound
At the beginning of a new project, a developer should pause and consider which methods are absolutely necessary for a class to operate properly. These methods should be declared abstract methods to avoid unexpected runtime errors in the future.
In my opinion this is especially important in non-compiled languages such as Python, which can be notoriously fragile to these errors.
Many popular data science and machine learning library use abstract base classes. One of those libraries is scikit-learn. The code snippet at the beginning of this post contains scikit-learn’s implementation of a “Kernel” base class. Kernels are used in Support Vector Machines, Kernel Ridge and Gaussian Process methods among others. They can be seen as a distance metric between two vectors. You might be familiar with the Radial Basis Function (RBF) or squared exponential kernel
but many others exist and are implemented in scikit-learn.
What all these kernels have in common, is that (in scikit-learn) they inherit from the Kernel ABC:
By making __call__, and is_stationary abstract methods, scikit-learn ensures that every new kernel will implement theses methods. At least in the case of __call__(self, X, Y), this is very reasonable, as a function to evaluate the kernel on two input matrices is absolutely necessary. Without it, the kernel would not have much use.
Wouldn’t it be great to automatically generate unit-tests for every new child of the Kernel class?
Thankfully, there is!
By creating a so-called registry we only need to implement a single unit-test once to apply it across our classes (one can do other fun things with registries, but I won’t get into any details here).
A registry simply keeps track of all the classes you create. To implement a registry in Python, we need to use meta-classes.
What are meta-classes?
One way to look at it is the following:
While a class tells Python how an instance of a class behaves, a meta-class defines the behavior of the class itself. [1]
So, by using a meta-class we can tell Python: “Create a new entry in the registry whenever a new class is implemented”
If all this sounds very confusing, you can also just copy paste the following code into your project:
(Note that in principle the registry does not have to inherit from ABCMeta but it usually makes sense to combine the two.)
Let’s go back to our Kernel example. Instead of inheriting from ABC, we now write
class Kernel(metaclass=ABCRegistry):       _registry_name = 'kernel'    ...class RBF(Kernel):    _registry_name = 'rbf'    ...class DotProduct(Kernel):    _registry_name = 'dotproduct'    ...
By creating the attribute _registry_name, we tell our registry how to call the classes. Accessing the registry will produce the following output:
Kernel.get_registry()>>> {'kernel': Kernel, 'rbf': RBF, 'dot': Dot}
Let’s say we want to test whether the kernels we implemented are symmetric:
K(X, Y) = K(Y, X)
Every valid kernel has to obey this condition, so it makes perfect sense to test for it.
Using pytest and its parametrize functionality we can now automatically apply the test to every class that inherits from “Kernel”:
Of course, one could always explicitly loop over all relevant classes. The use of a registry has the advantage that contributors adding new classes don’t need to worry about adding entries to the appropriate tests as well.
If you enjoyed this article please feel free to follow me here, on twitter or connect on LinkedIn.
References
[1] Loosely based on Thomas Wouters’ explanation on stackoverflow.