0

I've got a toy python example where defining an inline metaclass as a function behaves differently than defining it as a class, and I'm trying to understand why:

>>> class Test(object):
...     def __metaclass__(name, bases, nmspc):
...         cls = type(name, bases, nmspc)
...         def __init__(self, field, *args, **kwargs):
...             print "making " + field
...             super(cls, self).__init__()
...         cls.__init__ = __init__
...         return cls
... 
>>> t = Test('lol')
making lol
>>> class Test2(Test): pass
... 
>>> t = Test2('lol')
making lol

Versus:

>>> class Test(object):
...      class __metaclass__(type):
...              def __init__(cls, name, bases, nmspc):
...                      type.__init__(cls, name, bases, nmspc)
...                      def __init__(self, field, *args, **kwargs):
...                              print "making " + field
...                              super(cls, self).__init__()
...                      cls.__init__ = __init__
... 
>>> t = Test('lol')
making lol
>>> class Test2(Test): pass
... 
>>> t2 = Test2('lol')
making lol
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __init__
TypeError: __init__() takes at least 2 arguments (1 given)

So, what exactly is different between defining a class vs defining a function? They both end up returning a class through slightly different ways, since the modified cls.__init__ gets inherited one way but not the other.

I also checked similar construction by making a function and class declaration outside, and I get the same behavior.

1 Answers1

1

If you carefully read the documentation on metaclasses, you'll note that when Python checks for an inherited metaclass, it doesn't look at the __metaclass__ attribute of the base class, it looks for its __class__ or type (because in general, the metaclass of a class is its class). (You can see the C code implementing this behavior in the build_class function.)

Your function version calls type to construct the class, and then patches it afterwards. But the class's class is still set to type, because that's where it was actually built.

Your class version, on the other hand, subclasses type and itself becomes the __class__ of the created class.

You can see this difference (here I've named your function version TestF and your class version TestC, and imported them both from a module named mc):

In [7]: TestC.__class__
Out[7]: mc.__metaclass__

In [8]: TestF.__class__
Out[8]: type

In [9]: type(TestC)
Out[9]: mc.__metaclass__

In [10]: type(TestF)
Out[10]: type

Because of this difference, when you inherit from TestC, __metaclass__ is used as the metaclass for the new subclass, whereas when you inherit from TestF, type is used.

Why does this result in the observed difference? Because in the TestF case, since no custom metaclass is applied to the creation of the subclass, the subclass doesn't get a modified __init__ method applied. So the __init__ method called is still TestF.__init__(), and when it calls super(cls, self).__init__() it is calling object.__init__() which takes no arguments, and this works fine.

But in the TestC case, the metaclass is used again when you create the subclass. (You can observe this difference if you insert a print statement right before the cls.__init__ = __init__ line of each version.) So the subclass gets its own custom __init__ method. So in this case, when it calls super(cls, self).__init__(), it is calling TestC.__init__(), which (itself having been created by the metaclass), takes the required argument field, which you are not passing. And thus you get the TypeError: __init__() takes at least 2 arguments (1 given) error.

Carl Meyer
  • 122,012
  • 20
  • 106
  • 116