T.A.
T.A.

Reputation: 118

Does Python's builtin type.__init__(name,bases,dct) do anything?

I've seen some examples of Python metaclasses use a super() call to type.__init__(). What does this do?

Example:

class Meta(type):
    def __new__(cls, name, bases, dct):
        dct['a']='a'
        cls_obj = super(Meta, cls).__new__(cls, name, bases, dct)
        return cls_obj

    def __init__(cls_obj, name, bases, dct):
        cls_obj.b = 'b'
        dct['c'] = 'c'
        #what does this do
        super(Meta, cls_obj).__init__(name, bases, dct)

class Meta2(Meta):
    def __init__(cls_obj, name, bases, dct):
        cls_obj.b = 'b'

class Klass(metaclass=Meta):
    pass

class Klass2(metaclass=Meta2):
    pass


if __name__ == '__main__':
    print(Klass.a)
    print(Klass.b)
    print(Klass.c)

Output:

a
b
<...my system traceback...>
AttributeError: type object 'Klass' has no attribute 'c'

Clearly dct is not used to update Klass.__dict__. As far as I can tell, this doesn't do anything. Does it do something? Is there a reason you might want to include it? Is there any effective difference between Klass and Klass2?

Note: I'm talking specifically about the case when Meta inherits from type, not some custom superclass.

Upvotes: 2

Views: 524

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1121744

The type.__init__() implementation does indeed do nothing, other than validate the argument count and calling object.__init__(cls) (which again does nothing but a few sanity checks).

However, for metaclasses that are inherited from and have to account for other mixin metaclasses, using super().__init__(name, bases, namespace) ensures that all metaclasses in the MRO are consulted.

E.g. when using multiple metaclasses to base a new metaclass on, what is called via super().__init__() changes:

>>> class MetaFoo(type):
...     def __init__(cls, name, bases, namespace):
...         print(f"MetaFoo.__init__({cls!r}, {name!r}, {bases!r}, {namespace!r})")
...         super().__init__(name, bases, namespace)
...
>>> class MetaMixin(type):
...     def __init__(cls, name, bases, namespace):
...         print(f"MetaMixin.__init__({cls!r}, {name!r}, {bases!r}, {namespace!r})")
...         super().__init__(name, bases, namespace)
...
>>> class MetaBar(MetaFoo, MetaMixin):
...     def __init__(cls, name, bases, namespace):
...         print(f"MetaBar.__init__({cls!r}, {name!r}, {bases!r}, {namespace!r})")
...         super().__init__(name, bases, namespace)
...
>>> class Foo(metaclass=MetaFoo): pass
...
MetaFoo.__init__(<class '__main__.Foo'>, 'Foo', (), {'__module__': '__main__', '__qualname__': 'Foo'})
>>> class Bar(metaclass=MetaBar): pass
...
MetaBar.__init__(<class '__main__.Bar'>, 'Bar', (), {'__module__': '__main__', '__qualname__': 'Bar'})
MetaFoo.__init__(<class '__main__.Bar'>, 'Bar', (), {'__module__': '__main__', '__qualname__': 'Bar'})
MetaMixin.__init__(<class '__main__.Bar'>, 'Bar', (), {'__module__': '__main__', '__qualname__': 'Bar'})

Note how MetaMixin() is called last (before type.__init__() is called). If MetaMixin.__init__() were to consult the namespace dictionary for its own uses, then altering namespace in MetaFoo.__init__() would alter what MetaMixin.__init__() finds in that dictionary.

So for cases where you see super() being used in the __init__ method of a metaclass, you may want to check for more complex metaclass hierarchies. Or the project is just playing it safe and making sure that their metaclasses can be inherited from in more complex scenarios.

The namespace dictionary argument (you used the name dct) is copied before using it as the class attribute dictionary, so adding new keys to it in __init__ will not, indeed, alter the class dictionary.

Upvotes: 6

MisterMiyagi
MisterMiyagi

Reputation: 50076

The type.__init__ only ensures that:

  • no keyword arguments remain if exactly 1 positional argument is present,
  • either 1 or 3 positional arguments are supplied, and
  • object.__init__ is called, which
    • checks it was called correctly.

None of name, bases, or dct are actually used by either __init__ method. There are no side-effects beyond what the initial call already did (e.g. checking the validity of arguments).

As a result, skipping the call to super().__init__(name, bases, dct) does not have any negative side-effects if the superclass is type. However, similar to classes inheriting from object, calling super().__init__ is the proper design to stay future-proof - both for changes to the inheritance hierarchy and type.

Upvotes: 3

Related Questions