I'm trying to create a metaclass that can extend the mandatory params from its base class.
I have the following base class:
class BaseClass(JSONEncoder):
def __init__(self, field1, field2, ...):
self.field1 = field1
self.field2 = field2
...
There are about 10 params in total.
I am then creating a dict
object that contains all of the params and values to satisfy the BaseClass, and create my metaclass using:
# getMandatoryFields returns {'field1': 'value1', 'field2': 'value2, ...}
mandatory_fields = self.getMandatoryFields(my_data)
ChildClass = type('ChildClass', (BaseClass,), mandatory_fields)
However, before I even try to extend the dict, I am getting an error that all the required positional args from the BaseClass are missing:
TypeError: __init__() missing 10 required positional arguments: 'field1', 'field2',...
I am think this is to do with dict unpacking, but I cannot seem to get these values to be found. I assume that because I can pass in a class for inheritance to the type
constructor that this is possible, but I cannot seem to find the answer.
What your code does is to create a new class which has the default values you want as class attributes - alright. But it doesn't touch the __init__
function itself: it still requires 10 parameters - it won't know to pull default values from the class attributes.
You have to perceive that while it is common practice to receive parameters in a class' __init__
method and set instance attributes with that same names, that is just done in plain code: nothing(*) in the language itself links class (or instance) attribute names with the parameter names in __init__
.
(*) Well, "nothing" in the core language - although it is such a common practice, that late in the game Python threw in the dataclasses module which does exactly that: assume that you want an __init__
whose attributes mirror the ones declared in the class and auto-generate that.
If the __init__
method on your base class does just that, and call no other code, do not operate on the arguments received, no nothing, you can just use dataclasses in your problem - the dynamic class generated by calling type
will just override the defaults:
In [18]: @dataclasses.dataclass
...: class Base:
...: field1: str = ""
...: field2: int = 0
...:
In [19]: new_class = type("new_class", (Base,), {"field1": "foobar", "field2": 42})
In [20]: instance = new_class()
In [21]: instance
Out[21]: new_class(field1='', field2=0)
Perceive that you don't write an __init__
in the base class: dataclasses will generate one for you. If you have some base code to exectue in __init__
, dataclasses support a __post_init__
method, which receives only self
as an argument, and is called after the instance attributes are set: right your needed code in there.
(beware that this approach might not be compatible with more advanced use of dataclasses, such as using dataclasses.field
to create smarter default values in the base body)
If for some reason you can't use dataclasses (the base class is not under your control or it performs some calculations on the received arguments), then you will need to create a decorated version of that __init__
in the class you create.
The idea would be to build a decorator that will inspect the __init__
signature for parameter names, and try to extract those from the class attributes, if they were not passed in as **kwargs
, before calling the original __init__
. I think that dataclasses will just work in your case, though.