How to get default arguments from concrete __init__ into a metaclass' __call__?

Question:

Please consider

class Meta(type):
    def __call__(cls, *args, **kwargs):
        print(cls)
        print(f"args = {args}")
        print(f"kwargs = {kwargs}")
        super().__call__(*args, **kwargs)


class Actual(metaclass=Meta):
    def __init__(self, value=1):
        print(f"value=P{value}")


a1 = Actual()
a2 = Actual(value=2)

outputs

<class '__main__.Actual'>
args = ()
kwargs = {}
value=P1
<class '__main__.Actual'>
args = ()
kwargs = {'value': 2}
value=P2

Please notice kwargs = {} instead of kwargs = {'value': 1} as would be expected from the default __init__ argument in Actual.

How can I get the value in case a default was used, in the __call__ method?

Asked By: Gulzar

||

Answers:

The metaclass’__call__ will do that: call the class init, where the default value is stored: so it is only natural that this defaultvalue is not made avaliable upstream to it.

However, as it knows which __init__ method it will call, it can just introspection, through the tools offered in the inspect module to retrieve any desired defaults the about-to-be-called __init__ method has.

With this code you end up with the values that will be actually used in the __init__ method in the intermediate "bound_arguments" object – all arguments can be seem in a flat dictinary in this object’s .arguments attribute.

import inspect

class Meta(type):
    def __call__(cls, *args, **kwargs):
        # instead of calling `super.__call__` that would do `__new__` and `__init__` 
        # in a single pass, we need
        print(cls)
        sig = inspect.signature(cls.__init__)
        bound_args = sig.bind(None, *args, **kwargs)
        bound_args.apply_defaults()
        print(f"args = {bound_args.args}")
        print(f"kwargs = {bound_args.kwargs}")
        print(f"named arguments = {bound_args.arguments}")
        return super().__call__(*args, **kwargs)


class Actual(metaclass=Meta):
    def __init__(self, value=1):
        print(f"value=P{value}")


a1 = Actual()
a2 = Actual(value=2)

Output:

<class '__main__.Actual'>
args = (None, 1)
kwargs = {}
named arguments = {'self': None, 'value': 1}
value=P1
<class '__main__.Actual'>
args = (None, 2)
kwargs = {}
named arguments = {'self': None, 'value': 2}
value=P2

(Note that we do call super.__call__ with the original "args" and "kwargs"and not with the values in the intermediate object. In order to create the instance of BoundArguments used, this code passes "None" in place of "self": this just creates the appropriate argument and never run through the actual function.)

Answered By: jsbueno
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.