Класс данных Python из вложенного dict
Стандартная библиотека в 3.7 может рекурсивно преобразовывать класс данных в dict (пример из документов):
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
Я ищу способ превратить диктовку обратно в класс данных, когда есть вложение. Нечто подобное C(**tmp)
работает, только если поля класса данных являются простыми типами, а не самими классами данных. Я знаком с jsonpickle, который, однако, поставляется с заметным предупреждением безопасности.
Ответы
Ответ 1
Ниже приведена реализация 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:
# 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)])
Снова, как и ожидалось.
Ответ 2
Я автор dacite
- инструмента, упрощающего создание классов данных из словарей.
Эта библиотека имеет только одну функцию from_dict
- это быстрый пример использования:
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
Ответ 3
Вы можете использовать mashumaro для создания объекта класса данных из dict согласно схеме. 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
Ответ 4
Все, что нужно, - это пятислойный лайнер:
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
Ответ 5
Если ваша цель состоит в том, чтобы создавать JSON из существующих и заранее заданных классов данных, а затем просто написать пользовательские кодировщики и обработчики декодеров. Не используйте здесь dataclasses.asdict()
, вместо этого запишите в JSON (безопасную) ссылку на исходный класс данных.
jsonpickle
небезопасен, потому что он хранит ссылки на произвольные объекты Python и передает данные их конструкторам. С помощью таких ссылок я могу получить jsonpickle для ссылки на внутренние структуры данных Python, а также по желанию создавать и выполнять функции, классы и модули. Но это не значит, что вы не можете обращаться с такими ссылками небезопасно. Просто убедитесь, что вы только импортируете (не вызываете), а затем убедитесь, что объект является действительным типом класса данных, прежде чем использовать его.
Фреймворк можно сделать достаточно универсальным, но все же ограничить его только сериализуемыми JSON типами и dataclass
-based:
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, и при загрузке проверяется, что он по-прежнему является классом данных с теми же полями. Не проверяется тип по значениям полей (так как это совершенно другой котелок с рыбой).
Используйте их в качестве аргументов по default
и object_hook
для json.dump[s]()
и json.dump[s]()
:
>>> 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
с теми же хуками.
Вместо использования полностью подходящих имен модулей и классов, вы также можете использовать отдельный реестр для сопоставления допустимых имен типов; проверьте реестр на кодировку и еще раз на декодирование, чтобы убедиться, что вы не забыли зарегистрировать классы данных при разработке.
Ответ 6
undeictify - это библиотека, которая может помочь. Вот минимальный пример использования:
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)