Lately I've become a little bit obsessed with the question of whether you can create a fully functional metaclass that doesn't inherit from
type
(partly this was sparked by an @eevee tweet , although it's an issue I brushed against a while back ). It's not so much that I want to do this or think that it's sensible as that I can't prove what the answer is either way and that bugs me. But before I try to tackle the big issues I want to talk about what I mean by 'fully functional metaclass'.
Let's start with some very simple metaclasses, one of which inherits from
type
and one of which doesn't:
class M1(type):
def __new__(self, name, bases, dct):
print "M1", name
return super(M1, self).__new__(self, name, bases, dct)
class M2(object):
def __new__(self, name, bases, dct):
print "M2", name
return type(name, bases, dct)
class C1(object):
__metaclass__ = M1
class C2(object):
__metaclass__ = M2
M2
certainly looks like a metaclass despite not inheriting from
type
(eg if you try this out you can see that it is triggered on the creation of
C2
). But appearances are deceiving.
M2
is not a fully functional metaclass (and there are ways to demonstrate this). So let me show you what's really going on:
>>> type(C1) <class 'M1'> >>> type(C2) <type 'type'>
(We can get the same information by looking at each class's
__class__
attribute.)
The type of a class with a metaclass is the metaclass while the type of a class without a metaclass is
type
, and as we can see from this,
C2
doesn't actually have a metaclass. The reason for this is that
M2
created the actual class object for
C2
by calling
type()
directly, which does not give the newly created class a metaclass (instead it becomes a direct instance of
type
). If all you're interested in is changing a class as it's being created this may not matter, or at least you may not notice any side effects if you don't subclass your equivalent of
C2
. In this example
M1
is what I call a fully functional metaclass and
M2
is not. It looks like one and partly acts like one, but that is an illusion; at best it can do only one of the many things metaclasses can do . A fully functional metaclass like
M1
can do all of them.
Now let's come back to a demonstration that
M2
is not a real metaclass. The most alarming way to demonstrate this is to subclass both classes:
class C3(C1): pass class C4(C2): pass
If you try this out you'll see that
M1
is triggered when
C3
is created but
M2
is not triggered when
C4
is created.
This is very confusing because
C4
(and
C2
for that matter) has a visible
__metaclass__
attribute. It's just not meaningful after the creation of
C2
, contrary to what some documentation sometimes says. Note that this is sort of documented if you read Customizing class creation very carefully; see the section on precedence rules, which only talks about looking at a
__metaclass__
attribute in the actual class dictionary , not the class dictionaries of any base classes.
Note that this means that general callables cannot be true metaclasses . To create a true metaclass, one that will be inherited by subclasses, you must arrange for the created classes to be instances of you, and only classes can have instances. If you have a
__metaclass__
of, say, a function, it will be called only when classes explicitly list it as their metaclass; it will not be called for subclasses. This is going to surprise everyone except experts in Python arcana, so don't do that even if you think you have a use for it.
(If you do want to customize only classes that explicitly specify a
__metaclass__
attribute, do this check in your
__new__
function by looking at the passed in dictionary. Then people who read the code of your metaclass have a chance of working out what's going on.)
I will admit that Python 3 cleaned this up by removing the magic
__metaclass__
attribute. Now you can't be misled quite as much by the presence of
C2.__metaclass__
and the visibility of
C4.__metaclass__
. To determine whether something really has a metaclass in Python 3 you have no choice but to look at
type()
, which is always honest.