ネストされたdataclassオブジェクトを含むdataclassオブジェクトがあります。ただし、メインオブジェクトを作成すると、ネストされたオブジェクトが辞書になります。
@dataclass
class One:
f_one: int
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
f_three: str
f_four: One
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
two = Two(**data)
two
Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'})
obj = {'f_three': 'three', 'f_four': One(**{'f_one': 1, 'f_two': 'two'})}
two_2 = Two(**data)
two_2
Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'})
ご覧のとおり、すべてのデータを辞書として渡そうとしましたが、意図した結果が得られませんでした。次に、ネストされたオブジェクトを最初に作成してオブジェクトコンストラクターに渡そうとしましたが、同じ結果が得られました。
理想的には、次のようなものを取得するためにオブジェクトを構築したいと思います。
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
オブジェクト属性にアクセスするときはいつでも、ネストされた辞書を対応するデータクラスオブジェクトに手動で変換する以外に、それを達成する方法はありますか?
前もって感謝します。
これは、dataclasses
モジュール自体の複雑さと一致する複雑さを持つ要求です。つまり、この「ネストされたフィールド」機能を実現するためのおそらく最良の方法は、_@dataclass
_に似た新しいデコレータを定義することです。
幸運なことに、dataclass
を呼び出すことによってレンダリングされたクラスのように、フィールドとそのデフォルトを反映するために___init__
_メソッドのシグネチャが必要ない場合、これは非常に単純になる可能性があります。クラスデコレータ元のdataclass
を呼び出し、生成された___init__
_メソッドにいくつかの機能をラップします。これは、単純な "...(*args, **kwargs):
"スタイルの関数で実行できます。
つまり、必要なのは、生成された___init__
_メソッドのラッパーであり、「kwargs」で渡されたパラメーターを検査し、「データクラスフィールドタイプ」に対応するものがあるかどうかを確認し、該当する場合は、元の___init__
_を呼び出す前にネストされたオブジェクト。多分これはPythonよりも英語で書くのが難しいです:
_from dataclasses import dataclass, is_dataclass
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
_
___init__
_シグネチャを気にしないだけでなく、これは_init=False
_の受け渡しを無視することに注意してください。これはとにかく意味がないためです。
(戻り行のif
は、名前付きパラメーターで呼び出されるか、またはdataclass
自体のように直接デコレーターとして機能するためにこれを担当します)
そしてインタラクティブなプロンプトで:
_In [85]: @dataclass
...: class A:
...: b: int = 0
...: c: str = ""
...:
In [86]: @dataclass
...: class A:
...: one: int = 0
...: two: str = ""
...:
...:
In [87]: @nested_dataclass
...: class B:
...: three: A
...: four: str
...:
In [88]: @nested_dataclass
...: class C:
...: five: B
...: six: str
...:
...:
In [89]: obj = C(five={"three":{"one": 23, "two":"narf"}, "four": "zort"}, six="fnord")
In [90]: obj.five.three.two
Out[90]: 'narf'
_
署名を保持したい場合は、dataclasses
モジュール自体のプライベートヘルパー関数を使用して、新しい___init__
_を作成することをお勧めします。
dacite
モジュールを試すことができます。このパッケージは、辞書からのデータクラスの作成を簡素化します-ネストされた構造もサポートします。
例:
from dataclasses import dataclass
from dacite import from_dict
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a: A
data = {
'a': {
'x': 'test',
'y': 1,
}
}
result = from_dict(data_class=B, data=data)
assert result == B(a=A(x='test', y=1))
Daciteをインストールするには、単にpipを使用します。
$ pip install dacite
新しいデコレーターを作成する代わりに、実際のdataclass
が初期化された後で、dataclass
型のすべてのフィールドを変更する関数を思いつきました。
def dicts_to_dataclasses(instance):
"""Convert all fields of type `dataclass` into an instance of the
specified data class if the current value is of type dict."""
cls = type(instance)
for f in dataclasses.fields(cls):
if not dataclasses.is_dataclass(f.type):
continue
value = getattr(instance, f.name)
if not isinstance(value, dict):
continue
new_value = f.type(**value)
setattr(instance, f.name, new_value)
関数は手動または__post_init__
で呼び出すことができます。このようにして、@dataclass
デコレーターをすべての栄光の中で使用できます。
__post_init__
を呼び出した上記の例:
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
def __post_init__(self):
dicts_to_dataclasses(self)
f_three: str
f_four: One
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
two = Two(**data)
# Two(f_three='three', f_four=One(f_one=1, f_two='two'))
@jsbuenoによるソリューションの拡張を作成し、List[<your class/>]
の形式での入力も受け入れます。
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if isinstance(value, list):
if field_type.__Origin__ == list or field_type.__Origin__ == List:
sub_type = field_type.__args__[0]
if is_dataclass(sub_type):
items = []
for child in value:
if isinstance(child, dict):
items.append(sub_type(**child))
kwargs[name] = items
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper