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 に精通していますが、これには顕著なセキュリティ警告が付いています。
以下は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)])
再び期待どおり。
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
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
必要なのは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
目標がJSONから既存の事前定義済みデータクラスを生成することである場合、カスタムエンコーダーおよびデコーダーフック。ここではdataclasses.asdict()
を使用せず、代わりにJSONにを記録します元のデータクラスへの(安全な)参照。
jsonpickle
はarbitraryPythonオブジェクトへの参照を格納し、コンストラクターにデータを渡すため、安全ではありません。このような参照により、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
クラスのインスタンスを作成します。
完全修飾のモジュール名とクラス名を使用する代わりに、別のレジストリを使用して許容される型名をマッピングすることもできます。開発時にデータクラスを登録することを忘れないように、エンコード時、およびデコード時にレジストリをチェックしてください。
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)