python3 dataclass with **kwargs(asterisk)
The basic use case for dataclasses is to provide a container that maps arguments to attributes. If you have unknown arguments, you can't know the respective attributes during class creation.
You can work around it if you know during initialization which arguments are unknown by sending them to a catch-all attribute by hand:
from dataclasses import dataclass, field
@dataclass
class Container:
user_id: int
body: str
meta: field(default_factory=dict)
# usage:
obligatory_args = {'user_id': 1, 'body': 'foo'}
other_args = {'bar': 'baz', 'amount': 10}
c = Container(**obligatory_args, meta=other_args)
print(c.meta['bar']) # prints: 'baz'
But in this case you'll still have a dictionary you need to look into and can't access the arguments by their name, i.e. c.bar
doesn't work.
If you care about accessing attributes by name, or if you can't distinguish between known and unknown arguments during initialisation, then your last resort without rewriting __init__
(which pretty much defeats the purpose of using dataclasses
in the first place) is writing a @classmethod
:
@dataclass
class Container:
user_id: int
body: str
@classmethod
def from_kwargs(cls, **kwargs):
# split the kwargs into native ones and new ones
native_args, new_args = {}, {}
for name, val in kwargs.items():
if name in cls.__annotations__:
native_args[name] = val
else:
new_args[name] = val
# use the native ones to create the class ...
ret = cls(**native_args)
# ... and add the new ones by hand
for new_name, new_val in new_args.items():
setattr(ret, new_name, new_val)
return ret
# usage:
params = {'user_id': 1, 'body': 'foo', 'bar': 'baz', 'amount': 10}
Container(**params) # still doesn't work, raises a TypeError
c = Container.from_kwargs(**params)
print(c.bar) # prints: 'baz'
Dataclass only relies on the __init__
method so you're free to alter your class in the __new__
method.
from dataclasses import dataclass
@dataclass
class Container:
user_id: int
body: str
def __new__(cls, *args, **kwargs):
try:
initializer = cls.__initializer
except AttributeError:
# Store the original init on the class in a different place
cls.__initializer = initializer = cls.__init__
# replace init with something harmless
cls.__init__ = lambda *a, **k: None
# code from adapted from Arne
added_args = {}
for name in list(kwargs.keys()):
if name not in cls.__annotations__:
added_args[name] = kwargs.pop(name)
ret = object.__new__(cls)
initializer(ret, **kwargs)
# ... and add the new ones by hand
for new_name, new_val in added_args.items():
setattr(ret, new_name, new_val)
return ret
if __name__ == "__main__":
params = {'user_id': 1, 'body': 'foo', 'bar': 'baz', 'amount': 10}
c = Container(**params)
print(c.bar) # prints: 'baz'
print(c.body) # prints: 'baz'`
Here's a neat variation on this I used.
from dataclasses import dataclass, field
from typing import Optional, Dict
@dataclass
class MyDataclass:
data1: Optional[str] = None
data2: Optional[Dict] = None
data3: Optional[Dict] = None
kwargs: field(default_factory=dict) = None
def __post_init__(self):
[setattr(self, k, v) for k, v in self.kwargs.items()]
This works as below:
>>> data = MyDataclass(data1="data1", kwargs={"test": 1, "test2": 2})
>>> data.test
1
>>> data.test2
2
However note that the dataclass does not seem to know that is has these new attributes:
>>> from dataclasses import asdict
>>> asdict(data)
{'data1': 'data1', 'data2': None, 'data3': None, 'kwargs': {'test': 1, 'test2': 2}}
This means that the keys have to be known. This worked for my use case and possibly others.