2014-01-21
Fake versus real metaclasses and what a fully functional metaclass is
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.