来自嵌套结构的 Python 数据类

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,但是它有一个显著的安全警告。


编辑:

答案建议使用以下图书馆:

  • 英安岩
  • Mashumaro (我使用过一段时间,效果很好,但很快就遇到了棘手的角落情况)
  • Pydantic (工作得非常好,文档非常好,角落案例更少)
122763 次浏览

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

不出所料。

如果您的目标是在 存在的,预先定义的数据类之间生成 JSON,那么只需编写自定义编码器和解码器挂钩。这里不要使用 dataclasses.asdict(),而是记录对原始数据类的 在 JSON a (安全)引用。

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

使用以下参数作为 json.dump[s]()json.dump[s]()defaultobject_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

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

与使用完全限定的模块和类名不同,您还可以使用一个单独的注册中心来映射允许的类型名; 在编码和解码时检查注册中心,以确保您在开发时不会忘记注册数据类。

您可以使用 Mashumaro来根据方案从 dict 创建数据类对象。这个库中的 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

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

只需要一个五线针:

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

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)

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

对于给定字段的输入错误。

我建议用组合模式来解决这个问题, 主要的优点是您可以继续向这个模式添加类 让他们也这么做。

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

另外,您可以在 CompositeDect 类中使用一个工厂模式来处理其他情况,比如嵌套的 dicts、 tuple 等,这样可以节省大量样板文件。

不需要使用其他模块,就可以使用 __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

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

也支持列表的简单解决方案(并且可以扩展为其他通用用途)

from dataclasses import dataclass, asdict, fields, is_dataclass
from typing import List
from types import GenericAlias


def asdataclass(klass, d):
if not is_dataclass(klass):
return d
values = {}
for f in fields(klass):
if isinstance(f.type, GenericAlias) and f.type.__origin__ == list:
values[f.name] = [asdataclass(f.type.__args__[0], d2) for d2 in d[f.name]]
else:
values[f.name] = asdataclass(f.type,d[f.name])
return klass(**values)


@dataclass
class Point:
x: int
y: int


@dataclass
class C:
mylist: list[Point]
title: str = ""


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


assert c == asdataclass(C, asdict(c))


基于 https://stackoverflow.com/a/54769644/871166

我还没有看到提到的一个可能的解决方案是使用 dataclasses-json。这个库提供 dataclass实例到/从 JSON 的转换,以及到/从 dict的转换(如 dacitemashumaro,这是在前面的答案中建议的)。

除了 @dataclass之外,dataclasses-json还需要用 @dataclass_json装饰类。然后,修饰后的类获得两个成员函数,用于转换到/从 JSON 和/从 dict:

  • from_dict(...)
  • from_json(...)
  • to_dict(...)
  • to_json(...)

下面是问题中原始代码的一个稍微修改过的版本。我已经添加了从 dictPointC实例转换所需的 @dataclass_json修饰器和 assert:

from dataclasses import dataclass, asdict
from dataclasses_json import dataclass_json
from typing import List


@dataclass_json
@dataclass
class Point:
x: int
y: int


@dataclass_json
@dataclass
class C:
mylist: List[Point]


p = Point(10, 20)


assert asdict(p) == {'x': 10, 'y': 20}
assert p == Point.from_dict({'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
assert c == C.from_dict(tmp)

我真的认为这个概念在 这个的答案是最好的方法对这个问题。

我已经修改和开发了他的代码。这是从 dictionary 中加载数据类的正确函数:

def dataclass_from_dict(cls: type, src: t.Mapping[str, t.Any]) -> t.Any:
field_types_lookup = {
field.name: field.type
for field in dataclasses.fields(cls)
}


constructor_inputs = {}
for field_name, value in src.items():
try:
constructor_inputs[field_name] = dataclass_from_dict(field_types_lookup[field_name], value)
except TypeError as e:
# type error from fields() call in recursive call
# indicates that field is not a dataclass, this is how we are
# breaking the recursion. If not a dataclass - no need for loading
constructor_inputs[field_name] = value
except KeyError:
# similar, field not defined on dataclass, pass as plain field value
constructor_inputs[field_name] = value


return cls(**constructor_inputs)

然后你可以用下面的方法进行测试:

@dataclass
class Point:
x: float
y: float




@dataclass
class Line:
a: Point
b: Point


p1, p2 = Point(1,1), Point(2,2)
line = Line(p1, p1)


assert line == dataclass_from_dict(Line, asdict(line))

实际上,我知道现在可能已经有大量的 JSON 序列化库了,而且说实话,我可能碰巧读到这篇文章有点晚。然而,还有一个更新的(经过良好测试的)选项是 dataclass-wizard库。最近(在任何情况下都是两周前) ,从 v0.18.0版本开始,这个版本已经转移到了 生产/稳定状态。

它对于从 typing模块中输入泛型以及其他小众用例(如 Union类型中的数据类和带模式的日期和时间)提供了相当可靠的支持。我个人认为还有一些非常有用的特性,比如自动 钥匙套管转换(即从骆驼到蛇)和隐式 类型转换(即从字符串到带注释的 int)。

理想的用法是使用 JSONWizard Mixin 类,它提供有用的类方法,例如:

  • from_json
  • from_dict/from_list
  • to_dict
  • to_json/list_to_json

下面是在 Python 3.7 + 中测试过的一个不言自明的用法,其中包含了 __future__导入:

from __future__ import annotations


from dataclasses import dataclass
from dataclass_wizard import JSONWizard




@dataclass
class C(JSONWizard):
my_list: list[Point]




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




# Serialize Point instance
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)])
# default case transform is 'camelCase', though this can be overridden
# with a custom Meta config supplied for the main dataclass.
tmp = {'myList': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
assert c.to_dict() == tmp
assert C.from_dict(tmp) == c

NB : 值得注意的是,从技术上讲,您只需要对主数据类进行子类化,即序列化的模型; 如果需要,可以不使用嵌套的数据类。

如果不需要类继承模型,那么另一种选择是使用导出的辅助函数(如 fromdictasdict)根据需要将数据类实例转换为/从 Python dict对象转换。

添加一个替代选项-convtools 模型(医生/Github)。

这个图书馆的愿景是:

  • 先验证
  • 没有隐式类型转换
  • 在类型转换期间没有隐式数据丢失 -例如,将10.0转换为 int 即可,而将10.1转换为 int 则不然
  • 如果有一个模型实例,它就是有效的。

它还最好允许自动错误处理(链接)。

1. 只进行验证

from typing import List


from convtools.contrib.models import DictModel, build




class Point(DictModel):
x: int
y: int




class C(DictModel):
mylist: List[Point]




point, errors = build(Point, {"x": 10, "y": 20})
"""
>>> In [2]: point
>>> Out[2]: Point(x=10, y=20)
>>>
>>> In [3]: point.to_dict()
>>> Out[3]: {'x': 10, 'y': 20}
"""




obj, errors = build(C, {"mylist": [{"x": 0, "y": 0}, {"x": 10, "y": 4}]})
"""
>>> In [8]: obj
>>> Out[8]: C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])
>>>
>>> In [9]: obj.to_dict()
>>> Out[9]: {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
"""

2. 铸型

from convtools.contrib.models import cast




class Point(DictModel):
# no type casting here
x: int


# casting to int
#   - when run with no args it infers caster from output type
#   - OR you can pass built-in/custom casters, e.g. casters.IntLossy()
y: int = cast()




class Point(DictModel):
x: int
y: int


class Meta:
# forces all fields to be cast to expected types
cast = True


# # JIC: to override automatic caster inference:
# cast_overrides = {
#     date: casters.DateFromStr("%m/%d/%Y")
# }
from dataclasses import dataclass, is_dataclass


@dataclass
class test2:
a: str = 'name'
b: int = 222


@dataclass
class test:
a: str = 'name'
b: int = 222
t: test2 = None


a = test(a = 2222222222, t=test2(a="ssss"))
print(a)


def dataclass_from_dict(schema: any, data: dict):
data_updated = {
key: (
data[key]
if not is_dataclass(schema.__annotations__[key])
else dataclass_from_dict(schema.__annotations__[key], data[key])
)
for key in data.keys()
}
return schema(**data_updated)


print(dataclass_from_dict(test, {'a': 1111111, 't': {'a': 'nazwa'} }))

一个可能的替代方案可能是一个轻量级的辣椒库:

from dataclasses import dataclass, asdict
from typing import List


from chili import init_dataclass


@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


assert c == init_dataclass(tmp, C)

Chili 几乎支持整个类型模块,包括自定义泛型类型。 你可以在这里阅读更多 https://github.com/kodemore/chili

安装可以通过简单的运行通过 pip 或诗歌:

pip install chili

或者

poetry add chili

它只有一个依赖项,那就是键入扩展。