Search code examples
pythonmetaclass

why keyword argument are not passed into __init_subclass__(..)


Code:

class ExternalMeta(type):
    def __new__(cls, name, base, dct, **kwargs):
        dct['district'] = 'Jiading'
        x = super().__new__(cls, name, base, dct)
        x.city = 'Shanghai'
        return x


class MyMeta(ExternalMeta):
    def __new__(cls, name, base, dct, age=0, **kwargs):
        x = super().__new__(cls, name, base, dct)
        x.name = 'Jerry'
        x.age = age
        return x

    def __init__(self, name, base, dct, age=0, **kwargs):
        self.country = 'China'


class MyClass(metaclass=MyMeta, age=10):
    def __init_subclass__(cls, say_hi, **kwargs):
        print(f'keyword arguments are: {kwargs}')
        super().__init_subclass__(**kwargs)
        cls.hello = say_hi


class DerivedClass(MyClass, say_hi="hello"):
    pass

this throws:

Traceback (most recent call last):
  File "app2.py", line 27, in <module>
    class DerivedClass(MyClass, say_hi="hello"):
  File "app2.py", line 11, in __new__
    x = super().__new__(cls, name, base, dct)
  File "app2.py", line 4, in __new__
    x = super().__new__(cls, name, base, dct)
TypeError: __init_subclass__() missing 1 required positional argument: 'say_hi'

From the offical doc:

classmethod object.__init_subclass__(cls) This method is called whenever the containing class is subclassed. cls is then the new subclass. If defined as a normal instance method, this method is implicitly converted to a class method.

Keyword arguments which are given to a new class are passed to the parent’s class __init_subclass__. For compatibility with other classes using __init_subclass__, one should take out the needed keyword arguments and pass the others over to the base class, as in:

https://docs.python.org/3/reference/datamodel.html#object.\_\_init_subclass\_\_

I try to print kwargs, it's {}, empty, so why my say_hi, arguments are not passed to __init_subclass__ method?

edit: I read more materials and write an article with diagrams and tests about the creation of instance and classes in Python:

http://shanwq.com/?p=154

Reference to articles(including this page) are included in it, hopes it can help anyone.


Solution

  • Python exposes the mechanisms classes are created in the form of customizable meta-classes - and does not perform any magic beyond that.

    Which means: there is no "hidden channel" through which the keyword arguments of a class are passed to __init_subclass__ - that is done inside Python's type.__new__ call.

    When using __init_subclass__ one will typically do not combine it with a metaclass (one of the idea of the creation of the former was to reduce the need for metaclasses at all).

    In this case, you do use a metaclass, and you supress the keyword arguments in the call to type.__new__: there are no arguments it can convey to __init_subclass__.

    Simply pass the arguments there, and Python will be able to pass them down to __init_subclass__:

    ...
    class ExternalMeta(type):
        def __new__(cls, name, base, dct, **kwargs):
            dct['district'] = 'Jiading'
            # Forward the kwargs here:
            x = super().__new__(cls, name, base, dct, **kwargs)
            x.city = 'Shanghai'
            return x
    
    
    class MyMeta(ExternalMeta):
        def __new__(cls, name, base, dct, age=0, **kwargs):
            # and here:
            x = super().__new__(cls, name, base, dct, **kwargs)
            x.name = 'Jerry'
            x.age = age
            return x
    ...
    
    

    The extra kwargs arguments are also passed to the metaclass' __init__ method. But they follow a different "pipe". I will try to summarize the whole thing here:

    1. Keyword arguments are declared in the class statement. (ex. class MyClass(MyBase1, metaclass=MyMeta, extraarg="foobar"):

    2. The Python runtime will check the bases for the class and any "metaclass" argument to calculate the metaclass (if there is a metaclass conflict it will raise a typeerror)

    3. Python will then call the metaclass __prepare__ method, passing any extra arguments it got and use its return value as the namespace in which the class body will be executed (by default an ordinary dict)

    4. some special attributes (like __module__, __qualname__ and __anotations__ (with an empty dictionary)) are assigned in the namespace.

    5. the class body itself is executed: declared methods are created as functions and class attributes are assigned in the namespace provided by __prepare__.

    6. After the class body is executed, attribute annotations are assigned in the namespace __annotations__ entry, one by one. (not in any spec, could as well happen as each attribute is encountered, and is subject to change with PEP 649 implementation for Python 3.13)

    7. The Python runtime will call the metaclass ("MyMeta" in the example) passing it all arguments and keyword arguments, but for the argument named metaclass itself.

    8. Calling the metaclass means the __call__ method of the class of the metaclass will be executed. (The "twice removed metaclass" or "metametaclass"). This is ordinarily type itself, and is not usually customized, but for learning purposes.

    9. This __call__ method will get all the arguments and call the metaclass __new__ (this step is detailed bellow) and, if that returns an instance of the metaclass (i.e. an ordinary class), call the metaclass __init__ method - always passing the mandatory arguments + any named arguments.

    10. The __new__ method on the metaclass have, at some point, to call the super type.__new__ method: it is the only way for code written in Python (as opposed to code in an extension) to create a new class. In a custom metaclass, like in this example, the developer can remove, add or pre-process any extra arguments as desired - and forward them to type.__new__.

    11. type.__new__ in turn will: (1)calculate the class "MRO" (Method resolution order) (and yes, again - it was done prior to determine the metaclass, but the runtime has to confirm that a linearized MRO is possible here), including calling special methods for that, if one of the bases is a not a class, but features a __mro_entries__ special method, (2) create a new instance of the class,(3) call any existing descriptors in the passed namespace __set_name__ function, (4) call the class's most derived __init_subclass__ (i.e. the first __init_subclass__ it finds in a superclass), passing any extra arguments it got and (5) return the newly created class (to the "metametaclass" __call__)

    12. The metaclass __init__ is called, also with any extra arguments. By default it is type.__init__ which does nothing.

    13. The "metametaclass" __call__ returns the class that was returned by the metaclass __new__ call to the runtime

    14. if there are any class decorators, they are called with this returned class object

    15. the name given in the class statement is assigned to the class returned above in the context of the statement.

    
    class MetaMeta(type):
        def __call__(mcls, *args, **kwargs):
            print(f"Meta-meta-class __call__ with {mcls}, {args}, {kwargs}")
            result = super().__call__(*args, **kwargs)
            print("returning from meta-meta-class __call__")
            return result
    
    class Meta(type, metaclass=MetaMeta):
        @classmethod
        def __prepare__(mcls, *args, **kwargs):
            print(f"Metaclass __prepare__ with {mcls}, {args}, {kwargs}")
            class VerboseDict(dict):
                def __init__(self, name):
                    self.name = name
                def __setitem__(self, name, value):
                    print(f"{self.name} assignment {name}={value}")
                    if name == "__annotations__":
                        value = VerboseDict("   annotations")
                    super().__setitem__(name, value)
            return VerboseDict("ns")
        
        def __new__(mcls, name, bases, ns, **kwargs):
            print(f"metaclass __new__ with {mcls}, {name}, {bases}, {ns}, {kwargs}")
            result = super().__new__(mcls, name, bases, ns, **kwargs)
            print(f"Returning from the metaclass `__new__`")
            return result
        def __init__(cls, *args, **kwargs):
            print(f"metaclass __init__ with {cls}, {args}, {kwargs}")
            return super().__init__(*args, **kwargs)
        def __call__(cls, *args, **kwargs):
            print(f"metaclass __call__ (creating an instance) with {cls}, {args}, {kwargs}")
            return super().__call__(*args, **kwargs)
    
    class Descriptor:
        def __get__(self, instance, owner):
            return 23
        def __set_name__(self, owner, name):
            print(f"Descriptor __set_name__ with {self}, {owner}, {name}")
    
    def decorator(cls):
        print(f"class decorator with {cls}")
        return cls
    
    class Base:
        def __init_subclass__(cls, **kwargs):
            # one can't pass extra kwargs to this call: it will raise TypeError
            super().__init_subclass__()
            print(f"{__class__} __init_subclass__ with {cls}, {kwargs}")
    
    @decorator
    class MyClass(Base, metaclass=Meta, extra="foobar"):
        a = "foobar"
        b = Descriptor()
        c: int = 0
        d: str
        def e(self):
            __class__ # triggers the creatin of "__classcell__" attr in the namespace
    
    

    Output:

    Metaclass __prepare__ with <class '__main__.Meta'>, ('MyClass', (<class '__main__.Base'>,)), {'extra': 'foobar'}
    ns assignment __module__=__main__
    ns assignment __qualname__=MyClass
    ns assignment __annotations__={}
    ns assignment a=foobar
    ns assignment b=<__main__.Descriptor object at 0x7f47415f49e0>
    ns assignment c=0
       annotations assignment c=<class 'int'>
       annotations assignment d=<class 'str'>
    ns assignment e=<function MyClass.e at 0x7f4740c27880>
    ns assignment __classcell__=<cell at 0x7f47415f4280: empty>
    Meta-meta-class __call__ with <class '__main__.Meta'>, ('MyClass', (<class '__main__.Base'>,), {'__module__': '__main__', '__qualname__': 'MyClass', '__annotations__': {'c': <class 'int'>, 'd': <class 'str'>}, 'a': 'foobar', 'b': <__main__.Descriptor object at 0x7f47415f49e0>, 'c': 0, 'e': <function MyClass.e at 0x7f4740c27880>, '__classcell__': <cell at 0x7f47415f4280: empty>}), {'extra': 'foobar'}
    metaclass __new__ with <class '__main__.Meta'>, MyClass, (<class '__main__.Base'>,), {'__module__': '__main__', '__qualname__': 'MyClass', '__annotations__': {'c': <class 'int'>, 'd': <class 'str'>}, 'a': 'foobar', 'b': <__main__.Descriptor object at 0x7f47415f49e0>, 'c': 0, 'e': <function MyClass.e at 0x7f4740c27880>, '__classcell__': <cell at 0x7f47415f4280: empty>}, {'extra': 'foobar'}
    Descriptor __set_name__ with <__main__.Descriptor object at 0x7f47415f49e0>, <class '__main__.MyClass'>, b
    <class '__main__.Base'> __init_subclass__ with <class '__main__.MyClass'>, {'extra': 'foobar'}
    Returning from the metaclass `__new__`
    metaclass __init__ with <class '__main__.MyClass'>, ('MyClass', (<class '__main__.Base'>,), {'__module__': '__main__', '__qualname__': 'MyClass', '__annotations__': {'c': <class 'int'>, 'd': <class 'str'>}, 'a': 'foobar', 'b': <__main__.Descriptor object at 0x7f47415f49e0>, 'c': 0, 'e': <function MyClass.e at 0x7f4740c27880>, '__classcell__': <cell at 0x7f47415f4280: Meta object at 0x1e57400>}), {'extra': 'foobar'}
    returning from meta-meta-class __call__
    class decorator with <class '__main__.MyClass'>