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}