'Python: Use nested dict to create class of nested dataclass
In the basic case, one can easily map a dictionary to the parameters. The below shows the basic example.
def func1(x: int, y: int):
return x+y
input = {
"x": 1,
"y": 2,
}
## This Works
sum = func1(**input)
# sum = 3
Does Python provide any other types of shortcuts which would enable this type of behavior for nested classes?
from dataclasses import dataclass
@dataclass
class X:
x: int
@dataclass
class Y:
y: int
def func2(x: X, y: Y):
return x.x + y.y
input_2 = {
"X": {
"x": 1,
},
"Y": {
"y": 1,
},
}
sum = func2(**input_2)
# TypeError: func2() got an unexpected keyword argument 'X'
I have tried other approach's. This is an example fo something that works, but is not very general.
sum = func2(X(input_2[X][x]),Y(input_2[Y][y])
Also failed with pydantic
from pydantic import BaseModel
class X(BaseModel):
x: int
class Y(BaseModel):
y: int
def func2(x: X, y: Y):
return x.x + y.y
input_2 = {
"X": {
"x": 1,
},
"Y": {
"y": 1,
},
}
sum = func2(**input_2)
Solution 1:[1]
I think create a new class that includes X and Y, assume C can work for your case
from pydantic import BaseModel
class X(BaseModel):
x: int
class Y(BaseModel):
y: int
class C(X, Y):
pass
def func2(c: C):
x = c.x
y = c.y
return x + y
input_2 = C(**{
"x": 1,
"y": 1,
})
sum = func2(input_2)
print(sum)
Solution 2:[2]
You can use a decorator to convert each dict argument for a function parameter to its annotated type, assuming the type is a dataclass or a BaseModel in this case.
An example with the dataclass-wizard - which should also support a nested dataclass model:
import functools
from dataclasses import dataclass, is_dataclass
from dataclass_wizard import fromdict
def transform_dict_to_obj(f):
name_to_tp = {name: tp for name, tp in f.__annotations__.items()
if is_dataclass(tp)}
@functools.wraps(f)
def new_func(**kwargs):
for name, tp in name_to_tp.items():
if name in kwargs:
kwargs[name] = fromdict(tp, kwargs[name])
return f(**kwargs)
return new_func
@dataclass
class X:
x: int
@dataclass
class Y:
y: int
@transform_dict_to_obj
def func2(*, x: X, y: Y) -> str:
return x.x + y.y
input_2 = {
"x": {
"x": 1,
},
"y": {
"y": 1,
},
}
sum = func2(**input_2)
print('Sum:', sum)
assert sum == 2 # OK
Similarly, with pydantic:
import functools
from pydantic import BaseModel
class X(BaseModel):
x: int
class Y(BaseModel):
y: int
def transform_dict_to_obj(f):
name_to_from_dict = {name: tp.parse_obj
for name, tp in f.__annotations__.items()
if issubclass(tp, BaseModel)}
@functools.wraps(f)
def new_func(**kwargs):
for name, from_dict in name_to_from_dict.items():
if name in kwargs:
kwargs[name] = from_dict(kwargs[name])
return f(**kwargs)
return new_func
@transform_dict_to_obj
def func2(*, x: X, y: Y) -> str:
return x.x + y.y
input_2 = {
"x": {
"x": 1,
},
"y": {
"y": 1,
},
}
sum = func2(**input_2)
print('Sum:', sum)
assert sum == 2 # OK
For a slightly more optimized version, instead of using a for loop in the decorator each time, you can only add the logic you need to run, and then generate the new function using dataclasses._create_fn() or similar:
from dataclasses import dataclass, is_dataclass, _create_fn
from dataclass_wizard import fromdict
def transform_dict_to_obj_optimized(f):
args = []
body_lines = []
return_type = f.__annotations__.pop('return', None)
for name, tp in f.__annotations__.items():
type_name = tp.__qualname__
args.append(name)
if is_dataclass(tp):
body_lines.append(f'if {name}:')
body_lines.append(f' {name} = fromdict({type_name}, {name})')
body_lines.append(f'return original_fn({",".join(args)})')
return _create_fn(f.__name__, args, body_lines,
return_type=return_type,
locals={'original_fn': f},
globals=globals())
@dataclass
class X:
x: int
@dataclass
class Y:
y: int
@transform_dict_to_obj_optimized
def func2(x: X, y: Y) -> int:
return x.x + y.y
input_2 = {
"x": {
"x": 1,
},
"y": {
"y": 1,
},
}
sum = func2(**input_2)
print('Sum:', sum)
assert sum == 2 # OK
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Tony |
| Solution 2 |
