Abstract Base Classes

In the previous tutorial I showed you how to create a factory to build plugins and use new plugins without needed to make additional code changes. There is a downside to loading in plugins and in the way python works in general. If a plugin didn’t implement a function we expected, our program would crash when we tried to use that function. In other languages like C++ our program would fail to compile if our plugin didn’t implement our interface completely.

This tutorial will show what happens in this case in Python and show how we can avoid this.

Simple example:

Our program makes use of plugins, we want these plugins to all have a method called “do_something”, here is what happens if we load and use a plugin that doesn’t implement this method.

main:

from plugin_factory import PluginFactory
from plugins import *

pf = PluginFactory()
loaded_plugins = pf.get_registered_plugins()
for loaded_plugin in loaded_plugins:
  loaded_plugins[loaded_plugin].do_something()

Here we load our plugins and try and call do_something on each of them.

Let’s have a look at our plugins, first a valid one:

class PluginA(object):
  def __init__(self):
    pass

  @classmethod
  def name(cls):
    return "PluginA"

  def do_something(self):
    print "I am PluginA doing something"

PluginFactory().register_plugin(PluginA().name(), PluginA())

Now an invalid one, note it doesn’t have a do_something method:

from plugin_factory import PluginFactory

class PluginB(object):
  def __init__(self):
    pass

  @classmethod
  def name(cls):
    return "PluginB"

PluginFactory().register_plugin(PluginB().name(), PluginB())

When we run our main program we get the following output:

I am PluginA doing something
Traceback (most recent call last):
  File "main.py", line 7, in 
    loaded_plugins[loaded_plugin].do_something()
AttributeError: 'PluginB' object has no attribute 'do_something'

See how we already called do_something on PluginA, this means that we didn’t get the error about PluginB not having the correct method. Given that we might not actually try and use this method till much later on in our program it’s a not great that we weren’t told it was going to fail earlier.

A way around this problem is to inherit from a common class that ensures that all subclasses implement the do_something method. In order to enforce this in python you need to use the Abstract Base Class (abc) module, which has been available since Python 2.6.

First we make a base class for our plugins to inherit from, i_plugin.py

import abc

class IPlugin(object):
  __metaclass__ = abc.ABCMeta

  @abc.abstractmethod
  def do_something(self):
    return

Note the @abc.abstractmethod decorator, this is what is enforcing that a subclass must implement this method.

We then change our plugins to look like so:

from plugin_factory import PluginFactory
from i_plugin import IPlugin

class PluginA(IPlugin):
  def __init__(self):
    pass

  @classmethod
  def name(cls):
    return "PluginA"

  def do_something(self):
    print "I am PluginA doing something"

PluginFactory().register_plugin(PluginA().name(), PluginA())

If we do the same to PluginC, still leaving out the do_something method when we run out main we get the following output:

Traceback (most recent call last):
  File "main.py", line 2, in 
    from plugins import *
  File "/home/reuben/reuben.guru/tutorials/abstract-base-class/plugins/plugin_c.py", line 12, in 
    PluginFactory().register_plugin(PluginC().name(), PluginC())
TypeError: Can't instantiate abstract class PluginC with abstract methods do_something

Note in this run we didn’t get the line “I am PluginA doing something”, that is because the program terminated before it executed any of the code in our main.py. In my opinion this is good, now I will know straight away that something is wrong with one of my plugins, not at some point in the future when I want to use the missing method.

I’m sure there are lots of people that will say that this goes against what Python is about, I’m not here to argue this point. This works for me and in my opinion is a nicer solution.

Code on github

Leave a comment