-1

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?

Gulzar
  • 23,452
  • 27
  • 113
  • 201
  • Does this answer your question? [Get a function argument's default value?](https://stackoverflow.com/questions/12627118/get-a-function-arguments-default-value) – Silvio Mayolo Oct 01 '22 at 21:54
  • how are you using the keyword args to the init? that is, are you actually assigning them to fields. If so, I think refactoring to use a dataclass might potentially be helpful for this. – rv.kvetch Oct 01 '22 at 21:56
  • @SilvioMayolo This is a very heavy cannon for something that is supposed to come out of the box. I have to understand why there isn't a much simpler solution before doing something like that. – Gulzar Oct 01 '22 at 21:56
  • @rv.kvetch I don't understand your question [you see the entire code] and don't understand the suggestion, as my question was programming related, not a design one. – Gulzar Oct 01 '22 at 21:58
  • @Gulzar it's just i haven't seen a use case like this before, where you are passing keyword arguments to an init but not really using it anywhere. – rv.kvetch Oct 01 '22 at 22:00
  • @rv.kvetch This is clearly a stub case-study. I only show the values here, in the real program the values are used. – Gulzar Oct 01 '22 at 22:05
  • What are you trying to accomplish by doing this? Beware the [XY problem](https://meta.stackexchange.com/q/66377/343832); give the context. You might be able to get around this entirely by refactoring. – wjandrea Oct 01 '22 at 22:10
  • @wjandrea I just want to know the python language better. – Gulzar Oct 01 '22 at 22:13
  • 3
    @Gulzar Then why doesn't Silvio's link answer your question? It's effectively saying you have to use introspection. And why do you think this is "supposed to come out of the box" anyway? – wjandrea Oct 01 '22 at 22:34
  • In fact, something like this - i.e. determining param names to a function - is currently very hard to do in another language such as Rust (I would say it's impossible, but then I don't know Rust too well). So I feel it is great because in Python there is still a way to do this, even if it is kind of like bringing a heavy cannon (i.e. overkill) as you had mentioned. – rv.kvetch Oct 01 '22 at 22:44
  • @wjandrea Since the actual values do arrive at `__call__`, it would make sense that behind the scenes, the line `kwargs.update=current_args`. `current_args` not only can, but ARE evaluated and passed to `__call__` as shown above. I don't see why not evaluate the defaults beforehand. – Gulzar Oct 01 '22 at 22:50
  • 1
    @Gulzar I'm not sure what you mean with this `current_args`, but the default value is never passed to `Meta.__call__()`, if that's what you mean. – wjandrea Oct 02 '22 at 00:10

1 Answers1

1

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.)

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • When `Actual`'s `__init__` is defined as `def __init__(self, *args, **kwargs)`, the names `args` and `kwargs` appear in `bound_args`. Is there a canonical way of getting all the actual call variables, regardless of the origin of the argument (arg, kwarg, named arg, ect.)? – Gulzar Oct 03 '22 at 07:41
  • Also, I want for `def foo(x, y=1, **kwargs)` that is called as `foo(0, 1, z=2)` or `foo(x=0, z=2)` to have the same signature somehow. Is that doable with some builtins? – Gulzar Oct 03 '22 at 07:41