Abstract classes (or Interfaces) are an essential part of an Object-Oriented design. While Python is not a purely OOP language, it offers very robust solutions in terms of abstract and meta classes.
Abstract classes
In short, abstract classes are classes that cannot be instantiated. This is the case because abstract classes do not specify the implementation of features. Rather, you only specify the method signatures and (sometimes) property types, and every child class must provide its own implementation. Note that it is still possible to provide some implementation. For example, an abstract class can implement a default constructor.
Abstraction is very useful when designing complex systems to limit repetition and enforce consistency. For example, you may have the Renderer
abstract class, with implementations 3DRenderer
, 2DRenderer
, HapticRenderer
. Then, the code that is responsible for generating render data does not need to know about specific renderers: it will just refer to all of them as Renderer
.
ABC module
In Python, abstraction is realized through the abc
module in the built-in library. This is the simplest example of how to use it:
from abc import ABC
class AbstractRenderer(ABC):
pass
The abc
module exposes the ABC
class, which stands for A
bstract B
ase C
lass. Any class that inherits the ABC
class directly, is, therefore, abstract. But there would not be much use for it without abstract methods:
from abc import ABC, abstractmethod
class AbstractRenderer(ABC):
@abstractmethod
def render(self, data):
raise NotImplementedError()
In this example, we define the render
abstract method, hence the @abstractmethod
decorator. In its body, we raise the NotImplementedError
as a safety precaution: in theory, it should not be possible to even call this method. If you try to instantiate AbstractRenderer
, you will get an error:
>>> renderer = AbstractRenderer()
>>> Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class AbstractRenderer with abstract methods render
Unless there is a concrete implementation of every abstractmethod
in an abstract class, you will not be able to instantiate it. Here is how to implement it:
class 3DRenderer(AbstractRenderer):
def render(self, data):
some_library.draw_3d_stuff(data)
renderer = 3DRenderer()
renderer.render(some_data)
In addition to methods, it is possible to specify abstract properties (note that property
is not part of abc
):
class AbstractVehicle(ABC):
@property
@abstractmethod
def engine(self):
raise NotImplementedError()
@engine.setter
@abstractmethod
def engine(self, _engine):
raise NotImplementedError()
In this example, we define the engine
abstract property. The first function (decorated with @property
) is a getter, and the one decorated with @engine.setter
is the setter. Later, you will have to implement them both:
class Car(AbstractVehicle):
_engine = ''
@property
def engine(self):
return self._engine
@engine.setter
def engine(self, new_engine):
self._engine = new_engine.upper()
car = Car()
car.engine = 'v8 3.2 liters'
print(car.engine)
>> V8 3.2 LITERS
Take a closer look at the getter and the setter that is implemented here. Note that the setter changes the engine string to upper case. This is an example of using property
to override default Python behaviour and its usage with abc
.
Metaclasses
As you have just seen, Python’s implementation of abstract classes differs from what you see in other popular languages. This is because abstraction is not part of the language grammar, but instead is implemented as a library. The next logical question is, can you do something similar yourself, purely in Python? And the answer is yes.
Before moving to metaclasses, there is something you need to understand about classes first. You may think of classes as blueprints for object creation and you will be right. But, in Python, classes themselves are objects. For example, when running this code:
class YouExpectedMeToBeAClass:
pass
Python will instantiate a class YouExpectedMeToBeAClass
, and store this “object” in memory. Later, when you refer to this class when creating objects, python will use that “object”. But how does Python instantiate a class? Using a metaclass, of course.
Metaclasses are classes for classes. Metaclasses provide blueprints for classes creation. Every class has a metaclass by default (it is called type
). To create a custom metaclass, you will have to inherit type
:
class CustomMeta(type):
pass
class SomeClass(metaclass=CustomMeta):
pass
By itself, CustomMeta
does nothing. Let’s add some more features to show you the power of metaclasses. Let’s make CustomMeta
check if every child class has a render
attribute (like with AbstractRenderer
):
class CustomMeta(type):
def __new__(cls, clsname, bases, attrs):
if 'render' not in attrs.values():
raise Exception()
return type(clsname, bases, attrs)
class SomeClass(metaclass=CustomMeta):
pass
If you try to run this code (without even instantiating anything!) it will throw an error. Let me explain what __new__
is first. This is the constructor for classes, like __init__
for objects. It is called at the moment SomeClass
is defined and whatever is returned from this function becomes the class. The arguments are following: the metaclass (cls
), new classes’ name clsname
, parent classes (bases
) and attributes (attrs
). In function body, we enumerate the attributes and check if render
is one of them. If not, raise an exception. To make this code run, add this to SomeClass
:
def render(self):
pass
This is a very bare-bones example of what the abc
module does under the hood. If you are interested to learn more, these are it’s source codes: python module, c implementation.
Closing notes
Thank you for reading, I hope you liked my article. Let me know if you have any use cases for metaclasses in the comments!
Thank you, it’s a very clear and concise description of the topic. I’m trying to wrap my head around these concepts, and your article actually helped a lot.