Класс данных Python от dict - PullRequest
0 голосов
/ 19 ноября 2018

Стандартная библиотека в 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

Я ищу способ превратить dict обратно в класс данных, когда есть вложение,Нечто подобное C(**tmp) работает, только если поля класса данных являются простыми типами, а не самими классами данных.Я знаком с jsonpickle , который, однако, поставляется с заметным предупреждением безопасности.

Ответы [ 6 ]

0 голосов
/ 09 мая 2019

undictify - это библиотека, которая может помочь.Вот минимальный пример использования:

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 голосов
/ 19 февраля 2019

Все, что нужно, - это пять строк:

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, здесь, по сути: https://gist.github.com/gatopeich/1efd3e1e4269e1e98fae9983bb914f22

0 голосов
/ 10 декабря 2018

Я автор 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 поддерживает следующие функции:

  • вложенные структуры
  • (базовая) проверка типов
  • необязательные поля (т. Е. Ввод).
  • 1020 * объединения *
  • коллекция
  • значения приведения и преобразования
  • переназначение имен полей

... и это хорошо проверено - 100% покрытие кода!

Чтобы установить dacite, просто используйте pip (или pipenv):

$ pip install dacite
0 голосов
/ 28 ноября 2018

Вы можете использовать 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
0 голосов
/ 27 ноября 2018

Если ваша цель состоит в том, чтобы создать JSON из и до существующих, предварительно определенных классов данных, то просто напишите пользовательские перехватчики кодера и декодера. Не используйте dataclasses.asdict() здесь, вместо этого запишите в JSON (безопасную) ссылку на исходный класс данных.

jsonpickle небезопасен, поскольку хранит ссылки на произвольные объекты Python и передает данные их конструкторам. С помощью таких ссылок я могу получить 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-стиля для именования класса данных, и при загрузке подтверждается, что он по-прежнему является классом данных с теми же полями. Не проверяется тип по значениям полей (поскольку это совершенно другой котелок с рыбой).

Используйте их в качестве аргументов 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 с теми же хуками.

Вместо использования полностью подходящих имен модулей и классов, вы также можете использовать отдельный реестр для сопоставления допустимых имен типов; проверьте реестр на кодировку и еще раз на декодирование, чтобы убедиться, что вы не забыли зарегистрировать классы данных при разработке.

0 голосов
/ 27 ноября 2018

Ниже приведена реализация CPython asdict - или, в частности, внутренняя рекурсивная вспомогательная функция _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)])

Снова, как и ожидалось.

...