web-dev-qa-db-ja.com

Python dictのデータクラス

3.7の標準ライブラリは、データクラスを辞書に再帰的に変換できます(ドキュメントの例):

from dataclasses import dataclass, asdict
from typing import List

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: List[Point]

p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
tmp = {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
assert asdict(c) == tmp

ネストがあるときに、dictをデータクラスに戻す方法を探しています。 C(**tmp)のようなものは、データクラスのフィールドが単純なタイプであり、それ自体がデータクラスではない場合にのみ機能します。 jsonpickle に精通していますが、これには顕著なセキュリティ警告が付いています。

34
mbatchkarov

以下はasdictのCPython実装です。具体的には、使用する内部再帰ヘルパー関数_asdict_innerです。

# Source: https://github.com/python/cpython/blob/master/Lib/dataclasses.py

def _asdict_inner(obj, dict_factory):
    if _is_dataclass_instance(obj):
        result = []
        for f in fields(obj):
            value = _asdict_inner(getattr(obj, f.name), dict_factory)
            result.append((f.name, value))
        return dict_factory(result)
    Elif isinstance(obj, Tuple) and hasattr(obj, '_fields'):
        # [large block of author comments]
        return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
    Elif isinstance(obj, (list, Tuple)):
        # [ditto]
        return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
    Elif isinstance(obj, dict):
        return type(obj)((_asdict_inner(k, dict_factory),
                          _asdict_inner(v, dict_factory))
                         for k, v in obj.items())
    else:
        return copy.deepcopy(obj)

asdictはいくつかのアサーションで上記を呼び出し、デフォルトではdict_factory=dictを呼び出します。

コメントに記載されているように、これをどのように適合させて、必要なタイプタグ付きの出力辞書を作成できますか?


1。タイプ情報の追加

私の試みは、dictから継承するカスタムリターンラッパーを作成することでした。

class TypeDict(dict):
    def __init__(self, t, *args, **kwargs):
        super(TypeDict, self).__init__(*args, **kwargs)

        if not isinstance(t, type):
            raise TypeError("t must be a type")

        self._type = t

    @property
    def type(self):
        return self._type

元のコードを見ると、このラッパーを使用するために変更する必要があるのは最初の句のみです。他の句はdataclass- esのcontainersのみを処理するためです。

# only use dict for now; easy to add back later
def _todict_inner(obj):
    if is_dataclass_instance(obj):
        result = []
        for f in fields(obj):
            value = _todict_inner(getattr(obj, f.name))
            result.append((f.name, value))
        return TypeDict(type(obj), result)

    Elif isinstance(obj, Tuple) and hasattr(obj, '_fields'):
        return type(obj)(*[_todict_inner(v) for v in obj])
    Elif isinstance(obj, (list, Tuple)):
        return type(obj)(_todict_inner(v) for v in obj)
    Elif isinstance(obj, dict):
        return type(obj)((_todict_inner(k), _todict_inner(v))
                         for k, v in obj.items())
    else:
        return copy.deepcopy(obj)

輸入:

from dataclasses import dataclass, fields, is_dataclass

# thanks to Patrick Haugh
from typing import *

# deepcopy 
import copy

使用される機能:

# copy of the internal function _is_dataclass_instance
def is_dataclass_instance(obj):
    return is_dataclass(obj) and not is_dataclass(obj.type)

# the adapted version of asdict
def todict(obj):
    if not is_dataclass_instance(obj):
         raise TypeError("todict() should be called on dataclass instances")
    return _todict_inner(obj)

サンプルデータクラスを使用したテスト:

c = C([Point(0, 0), Point(10, 4)])

print(c)
cd = todict(c)

print(cd)
# {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

print(cd.type)
# <class '__main__.C'>

結果は期待どおりです。


2。dataclassに戻す変換

asdictで使用される再帰ルーチンは、いくつかの比較的小さな変更を加えて、逆プロセスに再利用できます。

def _fromdict_inner(obj):
    # reconstruct the dataclass using the type tag
    if is_dataclass_dict(obj):
        result = {}
        for name, data in obj.items():
            result[name] = _fromdict_inner(data)
        return obj.type(**result)

    # exactly the same as before (without the Tuple clause)
    Elif isinstance(obj, (list, Tuple)):
        return type(obj)(_fromdict_inner(v) for v in obj)
    Elif isinstance(obj, dict):
        return type(obj)((_fromdict_inner(k), _fromdict_inner(v))
                         for k, v in obj.items())
    else:
        return copy.deepcopy(obj)

使用される機能:

def is_dataclass_dict(obj):
    return isinstance(obj, TypeDict)

def fromdict(obj):
    if not is_dataclass_dict(obj):
        raise TypeError("fromdict() should be called on TypeDict instances")
    return _fromdict_inner(obj)

テスト:

c = C([Point(0, 0), Point(10, 4)])
cd = todict(c)
cf = fromdict(cd)

print(c)
# C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])

print(cf)
# C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])

再び期待どおり。

16
meowgoesthedog

dacite -辞書からのデータクラスの作成を簡素化するツールの作成者です。

このライブラリにはfrom_dict関数が1つしかありません-これは簡単な使用例です。

from dataclasses import dataclass
from dacite import from_dict

@dataclass
class User:
    name: str
    age: int
    is_active: bool

data = {
    'name': 'john',
    'age': 30,
    'is_active': True,
}

user = from_dict(data_class=User, data=data)

assert user == User(name='john', age=30, is_active=True)

さらに、daciteは次の機能をサポートしています。

  • 入れ子構造
  • (基本)型チェック
  • オプションのフィールド(つまり、入力。オプション)
  • 組合
  • コレクション
  • 値のキャストと変換
  • フィールド名の再マッピング

...そして、十分にテストされています-100%のコードカバレッジ!

Daciteをインストールするには、単にpip(またはpipenv)を使用します。

$ pip install dacite
20
Konrad Hałas

mashumaro を使用して、スキームに従って辞書からdataclassオブジェクトを作成できます。このライブラリのMixinは、便利なfrom_dictおよびto_dictメソッドをデータクラスに追加します。

from dataclasses import dataclass
from typing import List
from mashumaro import DataClassDictMixin

@dataclass
class Point(DataClassDictMixin):
     x: int
     y: int

@dataclass
class C(DataClassDictMixin):
     mylist: List[Point]

p = Point(10, 20)
tmp = {'x': 10, 'y': 20}
assert p.to_dict() == tmp
assert Point.from_dict(tmp) == p

c = C([Point(0, 0), Point(10, 4)])
tmp = {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
assert c.to_dict() == tmp
assert C.from_dict(tmp) == c
8
tikhonov_a

必要なのは5ライナーのみです。

def dataclass_from_dict(klass, d):
    try:
        fieldtypes = {f.name:f.type for f in dataclasses.fields(klass)}
        return klass(**{f:dataclass_from_dict(fieldtypes[f],d[f]) for f in d})
    except:
        return d # Not a dataclass field

サンプル使用法:

from dataclasses import dataclass, asdict

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Line:
    a: Point
    b: Point

line = Line(Point(1,2), Point(3,4))
assert line == dataclass_from_dict(Line, asdict(line))

JSONを含む完全なコード、ここGistで: https://Gist.github.com/gatopeich/1efd3e1e4269e1e98fae9983bb914f22

3
gatopeich

目標がJSONから既存の事前定義済みデータクラスを生成することである場合、カスタムエンコーダーおよびデコーダーフック。ここではdataclasses.asdict()を使用せず、代わりにJSONにを記録します元のデータクラスへの(安全な)参照。

jsonpicklearbitraryPythonオブジェクトへの参照を格納し、コンストラクターにデータを渡すため、安全ではありません。このような参照により、jsonpickleで内部Pythonデータ構造を参照し、関数、クラス、およびモジュールを自由に作成および実行できます。ただし、そのような参照を安全に処理できないわけではありません。インポートするだけで(呼び出しではなく)、オブジェクトが実際のデータクラス型であることを確認してから使用します。

フレームワークは十分に汎用的にすることができますが、それでもJSONシリアル化可能な型dataclassベースのインスタンスのみに制限されます:

import dataclasses
import importlib
import sys

def dataclass_object_dump(ob):
    datacls = type(ob)
    if not dataclasses.is_dataclass(datacls):
        raise TypeError(f"Expected dataclass instance, got '{datacls!r}' object")
    mod = sys.modules.get(datacls.__module__)
    if mod is None or not hasattr(mod, datacls.__qualname__):
        raise ValueError(f"Can't resolve '{datacls!r}' reference")
    ref = f"{datacls.__module__}.{datacls.__qualname__}"
    fields = (f.name for f in dataclasses.fields(ob))
    return {**{f: getattr(ob, f) for f in fields}, '__dataclass__': ref}

def dataclass_object_load(d):
    ref = d.pop('__dataclass__', None)
    if ref is None:
        return d
    try:
        modname, hasdot, qualname = ref.rpartition('.')
        module = importlib.import_module(modname)
        datacls = getattr(module, qualname)
        if not dataclasses.is_dataclass(datacls) or not isinstance(datacls, type):
            raise ValueError
        return datacls(**d)
    except (ModuleNotFoundError, ValueError, AttributeError, TypeError):
        raise ValueError(f"Invalid dataclass reference {ref!r}") from None

これは JSON-RPCスタイルのクラスヒント を使用してデータクラスに名前を付け、ロード時にこれが同じフィールドを持つデータクラスであることを確認します。フィールドの値に対して型チェックは行われません(これは魚のまったく異なるケトルです)。

これらをjson.dump[s]()およびjson.dump[s]()へのdefaultおよびobject_hook引数として使用します。

>>> print(json.dumps(c, default=dataclass_object_dump, indent=4))
{
    "mylist": [
        {
            "x": 0,
            "y": 0,
            "__dataclass__": "__main__.Point"
        },
        {
            "x": 10,
            "y": 4,
            "__dataclass__": "__main__.Point"
        }
    ],
    "__dataclass__": "__main__.C"
}
>>> json.loads(json.dumps(c, default=dataclass_object_dump), object_hook=dataclass_object_load)
C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])
>>> json.loads(json.dumps(c, default=dataclass_object_dump), object_hook=dataclass_object_load) == c
True

または、同じフックを持つ JSONEncoder および JSONDecoder クラスのインスタンスを作成します。

完全修飾のモジュール名とクラス名を使用する代わりに、別のレジストリを使用して許容される型名をマッピングすることもできます。開発時にデータクラスを登録することを忘れないように、エンコード時、およびデコード時にレジストリをチェックしてください。

3
Martijn Pieters

ndictify は役立つライブラリです。最小限の使用例を次に示します。

import json
from dataclasses import dataclass
from typing import List, NamedTuple, Optional, Any

from undictify import type_checked_constructor


@type_checked_constructor(skip=True)
@dataclass
class Heart:
    weight_in_kg: float
    Pulse_at_rest: int


@type_checked_constructor(skip=True)
@dataclass
class Human:
    id: int
    name: str
    nick: Optional[str]
    heart: Heart
    friend_ids: List[int]


tobias_dict = json.loads('''
    {
        "id": 1,
        "name": "Tobias",
        "heart": {
            "weight_in_kg": 0.31,
            "Pulse_at_rest": 52
        },
        "friend_ids": [2, 3, 4, 5]
    }''')

tobias = Human(**tobias_dict)
0
Tobias Hermann