0

getInitInpArgs on ann gets the input arguments in init of ann to a dictionary. Now I want to define a function like getInitInpArgs on ann (parent class) and not on the child classes, which makes a dictionary of input arguments of init of myAnn(child class).

import inspect

class ann():
    def __init__(self, arg1):
        super(ann, self).__init__()
        self.getInitInpArgs()

    def getInitInpArgs(self):
        args, _, _, values = inspect.getargvalues(inspect.currentframe().f_back)
        self.inputArgs = {arg: values[arg] for arg in args if arg != 'self'}

class myAnn(ann):
    def __init__(self, inputSize, outputSize):
        super(myAnn, self).__init__(4)

z1=myAnn(40,1)
print(z1.inputArgs)

So here we either would have z1.inputArgs equal to {'arg1': 4, 'inputSize':40, 'outputSize':1} or equal to {'inputSize':40, 'outputSize':1} but now z1.inputArgs is equal to {'arg1': 4}.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
Farhang Amaji
  • 742
  • 11
  • 24

1 Answers1

3

about "how" you are trying to do it:

no.

Not sure what you are trying to do there, but this is the wrong way.

Instantiating classes and subclasses, and annotating passed arguments is a basic function of coding in Python.

Inspecting the variables in the scope of caller frames, while possible and documented in Python is an advanced feature, that while perfectly ok to be used when needed, should not be required to do the basic - it is an extraordinary feat, and as so, it should be reserved for extraordinary things. One of the benefits of it being possible is that writing semi-magical libraries frameworks which allow the basic use of ORMs, test frameworks, profiling, and such to be even more basic and easy.

But it is not a resource to be used lightly, and not for checking arguments passed to a child class - this is just a matter of understanding better the way parameters and variables are dealt with, and write your code accordingly.

Here is what is wrong

So, what is "not expected" in your code, is that you'd expect the getInitArgs method to anotate automatically all the parameters passed to __init__. But you do that by using a super-advanced feature in the language to inspect the arguments passed in a function in a Frame object - and then you always pick the frame prior to the current one - i.e. - inspect.currentframe().f_back is hardcoded to pick the frame of the function which called getInitArgs - but this function is the method in the parent class - the __init__ of the child class is two levels removed at this point (it could be accessed by doing inspect.currentframe().f_back.f_back) As you can see, that is not feasible in large projects: you'd need a complicated logic there just to check how far removed the initial __init__ is to get the arguments from the "correct" __init__, and it would have a lot of edge cases.

what should be done:

Instead, for you write a mechanism to annotate all passed arguments, even when you are in a superclass that knows nothing about the argument names in the child classes, there is an "intermediate" Python feature which are the "keyword arguments" - For a coincidence, I wrote a lengthy answer explaining their working, and the "one obvious way" to retrieve different initialization arguments in a large class hierarchy last week - so I will refer you there:

Python Hybrid Inheritance

To do exactly what you are trying to - but without frame introspection -

That answer talks about the common way of dealing with several child-classes, each having its own specific parameters - but on the more common pattern, there is not a "catch all" - each __init__ method would know about, and take care, of the parameters it is concerned with. (But check Python's dataclasses, about generating the __init__ methods automatically for that: https://docs.python.org/3/library/dataclasses.html )

getting the exact functionality you are expecting:

If that is not desired at all, another way to do it is to have each __init__ in each child-class to be decorated with some code that would annotate the initial parameters, and just then call the __init__ function proper. That can be done with the __init_subclass__ special method - I think it is more maintainable than your approach there:

from functools import wraps

# class and method names normalized to a more usual 
# Python naming scheme 

class Base:

    @staticmethod
    def get_initial_args_wrapper(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            # This code will run automatically before each __init__ method
            # in each subclass: 
            
            # if you want to annotate arguments passed in order,
            # as just "args", then you will indeed have to resort
            # to the "inspect" module - 
            # but, `inspect.signature`, followed by `signature.bind` calls 
            # instead of dealing with frames.
    
            # for named args, this will just work to annotate all of them:
            input_args = getattr(self, "input_args", {})
            
            input_args.update(kwargs)
            self.input_args = input_args
    
            # after the arguments are recorded as instance attributes, 
            # proceed to the original __init__ call:
            return func(self, *args, **kwargs)
        return wrapper
        
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if "__init__" in cls.__dict__:
            setattr(cls, "__init__", cls.get_initial_args_wrapper(cls.__init__))

And running it with your example (with "normalized" Python names):

  class Ann(Base):
    def __init__(self, arg1):
        super().__init__()
    
class MyAnn(Ann):
    def __init__(self, input_size, output_size):
        super().__init__(arg1=4)
z1=MyAnn(input_size=40, output_size=1)
print(z1.input_args)
# outputs:

{'input_size': 40, 'output_size': 1, 'arg1': 4}


jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • this is so dependent on passing the arguments with thier names and doesnt work when we define `z1=MyAnn(40, 1)` instead of `z1=MyAnn(input_size=40, output_size=1)`. I gave a thumbs up for respect and effort that u dear have put but its not what I wanted, I want to capture things when they are not paid attention to. like when the other users are gonna use my base class, if they dont obey passing arguments with their names, my code of capturing input args still works – Farhang Amaji May 30 '23 at 09:20
  • So, read it again: there are comments in the code indicating what calls from inspect you have to do and where to place them to get this behavior. – jsbueno May 30 '23 at 12:55
  • first of all yesterday I had solve the problem by bypassing it with a way I didnt like it (forced the child class to pass the frame!!). I was also so much busy therefore literally I didnt read ur answer carefully but my appreciations there. from other hand I didnt know that `inspect.getargvalues(inspect.currentframe().f_back.f_back)` actually I checked it and it didnt work but now works!!. now Ive found `.mro()` which with looping through `self.__class__.mro()[i]==ann` I may find the number of times `.f_back` is needed. I may post my answer tomorrow. but will be glad to hear ur comment. – Farhang Amaji May 30 '23 at 22:25
  • At the point the code evolved in my last answer, it is not that far apart from inspecting frames - so, if you get this approach working enough for your needs,go for it. Be aware that locating the right "initial `__init__`" using f-back can have unexpected corner cases - (for example, if one such function is decorated, and therefore inside a wrapper). Anyway, both approaches are using `inspect` calls to find the argument names. – jsbueno May 30 '23 at 22:39