嵌套字典中的Python数据类


72

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,但是它带有突出的安全警告。


标记为重复的问题确实是在问同样的问题,但此处给出的答案不适用于此特定示例。我在这里留下了评论,但仍在寻找更一般的答案。
mbatchkarov

您能在这里明确说明这种区别吗?看来您可能必须在其中添加,elifif检查各种提示。我不确定您如何将其归纳为任意类型的提示(例如,Dict以及Tuple除了List
Patrick Haugh

5
asdict正在丢失信息。在一般情况下,这是不可能的。
wim

6
具体来说,asdict不存储有关该字典从哪个类生成的任何信息。给定class A: x: intclass B: x: int{'x': 5}应用于创建A或的实例B。您似乎在假设属性名称列表唯一地定义了一个列表,并且现有名称到数据类的映射可以用来选择正确的类。
chepner

2
我建议您检查一下这个
阿卜杜勒·尼亚斯

Answers:


37

下面是它使用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)])

再次如预期。


9
TL; DR,+ 1为答案的全面性。
iBug

3
+0:尝试+1,但是-1,因为从一开始它基本上是一个坏主意。
wim

1
@wim我同意tbh-除了理论上的练习(它至少表明它dataclass与现有对象类型配合得很好)之外,别无其他。
meowgoesthedog18年

我将接受这一点,因为它是最全面的答案,可以帮助未来的用户了解问题的核心。最后我得到了更接近@Martijn的建议,因为我确实想要JSON。谢谢大家的回答
mbatchkarov

64

我是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

2
太棒了!我们如何建议将此功能添加到python标准库中?:-)
Alex

好人@Konrad :)
Gideon

这很棒!希望尽快将其添加到本机Python数据类中
Esostack

1
我不明白为什么Python会带来数据类,但没有增加从包含嵌套类的字典中创建数据类的可能性。
侏罗纪

20

它只需要五个班轮即可:

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


9

您可以使用mashumaro根据该方案从字典创建数据类对象。该库中的Mixin为数据类添加了方便from_dictto_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

1
哇,太好了 如果对msgpack和pyyaml的依赖关系是可选的,那么有时我可以看到它已包含在标准库中。将序列化添加到数据类非常简单,这可能是最开始使用它们的最常见原因之一。
Giorgio Balestrieri

6

如果您的目标是从现有的预定义数据类中生成JSON,则只需编写自定义编码器和解码器挂钩即可。不要在这里使用,而是在JSON中记录对原始数据类的(安全)引用。dataclasses.asdict()

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样式的类提示来命名数据类,并且在加载时被验证仍然是具有相同字段的数据类。不会对字段的值进行类型检查(因为这是完全不同的鱼)。

使用这些作为defaultobject_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

或使用相同的钩子创建JSONEncoderJSONDecoder类的实例。

除了使用完全限定的模块和类名称之外,还可以使用单独的注册表来映射允许的类型名称。检查注册表的编码,然后再检查解码,以确保您在开发时不会忘记注册数据类。


3

无需使用其他模块,就可以利用该__post_init__功能将dict值自动转换为正确的类型。此函数在之后调用__init__

from dataclasses import dataclass, asdict


@dataclass
class Bar:
    fee: str
    far: str

@dataclass
class Foo:
    bar: Bar

    def __post_init__(self):
        if isinstance(self.bar, dict):
            self.bar = Bar(**self.bar)

foo = Foo(bar=Bar(fee="La", far="So"))

d= asdict(foo)
print(d)  # {'bar': {'fee': 'La', 'far': 'So'}}
o = Foo(**d)
print(o)  # Foo(bar=Bar(fee='La', far='So'))

该解决方案的另一个好处是能够使用非数据类对象。只要它的str功能可以转换回来,它就是公平的游戏。例如,它可用于将str字段保留为IP4Address内部字段。


0

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

Validobj就是这样做的。与其他库相比,它提供了一个更简单的界面(目前只是一个功能),并强调了信息丰富的错误消息。例如,给定一个类似

import dataclasses
from typing import Optional, List


@dataclasses.dataclass
class User:
    name: str
    phone: Optional[str] = None
    tasks: List[str] = dataclasses.field(default_factory=list)

一个像这样的错误

>>> import validobj
>>> validobj.parse_input({
...      'phone': '555-1337-000', 'address': 'Somewhereville', 'nme': 'Zahari'}, User
... )
Traceback (most recent call last):
...
WrongKeysError: Cannot process value into 'User' because fields do not match.
The following required keys are missing: {'name'}. The following keys are unknown: {'nme', 'address'}.
Alternatives to invalid value 'nme' include:
  - name

All valid options are:
  - name
  - phone
  - tasks

给定字段上的错字。


0
from validated_dc import ValidatedDC
from dataclasses import dataclass

from typing import List, Union


@dataclass
class Foo(ValidatedDC):
    foo: int


@dataclass
class Bar(ValidatedDC):
    bar: Union[Foo, List[Foo]]


foo = {'foo': 1}
instance = Bar(bar=foo)
print(instance.get_errors())  # None
print(instance)               # Bar(bar=Foo(foo=1))

list_foo = [{'foo': 1}, {'foo': 2}]
instance = Bar(bar=list_foo)
print(instance.get_errors())  # None
print(instance)               # Bar(bar=[Foo(foo=1), Foo(foo=2)])

validated_dc:https :
//github.com/EvgeniyBurdin/validated_dc

并查看更详细的示例:https :
//github.com/EvgeniyBurdin/validated_dc/blob/master/examples/detailed.py


-1

我想建议使用Composite Pattern解决此问题,主要优点是您可以继续向该模式添加类,并使它们的行为相同。

from dataclasses import dataclass
from typing import List


@dataclass
class CompositeDict:
    def as_dict(self):
        retval = dict()
        for key, value in self.__dict__.items():
            if key in self.__dataclass_fields__.keys():
                if type(value) is list:
                    retval[key] = [item.as_dict() for item in value]
                else:
                    retval[key] = value
        return retval

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


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


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

附带说明一下,您可以在CompositeDict类中采用一种工厂模式,该模式可以处理其他情况,例如嵌套字典,元组等,这可以节省很多样板。


这个解决方案很差,太复杂了,您应该使用“ dacite”之类的外部库。
侏罗纪

@jurass,如果OP想要使用一个图书馆,他不会问这个问题
NOOBAF
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.