web-dev-qa-db-ja.com

Pythonでネストされたdataclassオブジェクトを作成する

ネストされた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'))

オブジェクト属性にアクセスするときはいつでも、ネストされた辞書を対応するデータクラスオブジェクトに手動で変換する以外に、それを達成する方法はありますか?

前もって感謝します。

8
mohi666

これは、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___を作成することをお勧めします。

10
jsbueno

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
9
Konrad Hałas

新しいデコレーターを作成する代わりに、実際の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'))
3
Yourstruly

@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
1
Daan Luttik