Using a Metaclass to Keep Track of Subclasses
A few days ago I read Eli Bendersky’s blog post titled “Fundamental concepts of plugin infrastructures” which covers 4 fundamental aspects of any plugin infrastructure. It’s a great article, and it gives an example in Python which implements the concepts he talks about. One aspect of his example I’d like to focus on in this post is how he uses a metaclass to allow plugins (which are subclasses of a class using the metaclass) to register themselves in the system.
The following code is what I’ll be talking about:
class Registry(type):
entries = []
def __init__(cls, name, bases, attrs):
if name != "Entry":
Registry.entries.append(cls)
class Entry(metaclass=Registry):
pass
Using this code we can keep track of subclasses of Entry
:
>>> Registry.entries
[]
>>> class Example(Entry):
... pass
...
>>> Registry.entries
[__main__.Example]
If we’re using this for a plugin system as in Eli’s case, then subclassing Entry is a way of registering the plugin with the system.
What’s going on here?
A metaclass is a class which instantiates classes. Since everything (including
classes) in Python is an object, everything must have a class which
instantiates it. As usual, we can use type()
to discover this class.
>>> class X:
... pass
...
>>> type(X)
<class 'type'>
This means that regular classes are actually instances of type
, and this
makes sense since we can use type()
to instantiate a “type object” (i.e. a
class). Doing so gives us an equivalent class to what we previously created:
>>> X = type('X', (object,), {})
>>> type(X)
<class 'type'>
So if we want to automatically keep track of classes which subclass Entry
,
we want to modify the class creation process to append the class to a list.
Since type
is the class from which regular classes are instantiated, we’ll
do what we usually do when we want access to the object creation process: we’ll
subclass type
!
A Registry
is a subclass of type
, we can use it in the same way as we
just use type()
:
>>> ExampleEntry = Registry("ExampleEntry", (object,), {})
>>> type(ExampleEntry)
__main__.Registry
>>> Registry.entries
[__main__.ExampleEntry]
As expected, the __init__
method of Registry
was called, and we were
able to save the class object that was passed in. It’s important to note that
the first parameter of __init__
is the class after it was created, just
an __init__
on regular classes gets the object after it was created; this
is sufficient for our use case, so we won’t delve into the other magic methods.
The equivalent to the above syntax is the following:
class ExampleEntry(object, metaclass=Registry):
pass
Since we want plugin authors to always subclass Entry
, we gave Entry
the metaclass parameter (which all subclasses will inherit), thus ensuring that
all subclasses of Entry
get automatically added to Registry.entries
. Of
course, we don’t want Entry
itself in that list, so we only add classes
which don’t have the name “Entry”.
More info
Hopefully this article provided a good use case for metaclasses and explained how it worked. For more information on metaclasses, I suggest Ionel Mărieș’s blog post on the topic as a good starting point since he also provides links to more articles in the first footnote.