Python 来自嵌套字典的数据类
Python dataclass from a nested dict
3.7 中的标准库可以递归地将数据class 转换为字典(示例来自文档):
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
我正在寻找一种在存在嵌套时将字典转回数据的方法class。像 C(**tmp)
这样的东西只有在数据 class 的字段是简单类型而不是它们本身是数据 class 的情况下才有效。我熟悉 [jsonpickle][1],但是它带有一个突出的安全警告。
编辑:
答案建议了以下库:
- 英安岩
- mashumaro(我用了一段时间,效果很好,但我很快 运行 陷入棘手的极端情况)
- pydantic(工作得很好,优秀的文档和更少的角落案例)
[1]: https://jsonpickle.github.io/
下面是 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
。
How can this be adapted to create an output dictionary with the required type-tagging, as mentioned in the comments?
1.添加类型信息
我的尝试涉及创建一个自定义 return 包装器,继承自 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
查看原始代码,只需修改第一个子句即可使用此包装器,因为其他子句仅处理 containers of 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)])
再次如预期。
如果您的目标是从 现有的预定义 数据class 生成 JSON,然后只需编写自定义编码器和解码器挂钩。不要在这里使用 dataclasses.asdict()
,而是在 JSON 中记录 对原始数据 class.
的(安全)引用
jsonpickle
不安全,因为它存储对 任意 Python 对象的引用,并将数据传递给它们的构造函数。有了这些引用,我可以让 jsonpickle 引用内部 Python 数据结构并随意创建和执行函数、classes 和模块。但这并不意味着您不能不安全地处理此类引用。只需验证您只导入(而不是调用),然后验证该对象是实际数据class类型,然后再使用它。
框架可以变得足够通用,但仍然仅限于 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-style class hints 命名数据 class,并且在加载时验证它仍然是具有相同字段的数据 class。没有对字段的值进行类型检查(因为那是完全不同的鱼缸)。
将它们用作 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
and JSONDecoder
classes 的实例。
除了使用完全限定的模块和 class 名称,您还可以使用单独的注册表来映射允许的类型名称;检查注册表中的编码,并再次检查解码,以确保您在开发时不会忘记注册数据classes。
您可以使用 mashumaro 根据方案从字典创建数据类对象。来自这个库的 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
我是 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
支持以下功能:
- 嵌套结构
- (基本)类型检查
- 可选字段(即 typing.Optional)
- 工会
- collections
- 值转换和转换
- 重新映射字段名称
... 并且经过充分测试 - 100% 代码覆盖率!
要安装英安岩,只需使用 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))
完整代码,包括 to/from 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
给定字段中的拼写错误。
我想建议使用复合模式来解决这个问题,
主要优点是您可以继续向此模式添加 classes
并让他们以同样的方式行事。
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 class 中使用工厂模式来处理其他情况,例如嵌套的字典、元组等,这将节省大量样板文件。
无需额外模块,您可以使用 __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))
基于
我还没有看到提到的一个可能的解决方案是使用dataclasses-json
. This library provides conversions of dataclass
instances to/from JSON, but also to/from dict
(like dacite
and mashumaro
,这在之前的答案中被建议过)。
dataclasses-json
除了 @dataclass
之外,还需要用 @dataclass_json
装饰 类。修饰后的 类 然后得到几个成员函数用于转换 to/from JSON 和 to/from dict
:
from_dict(...)
from_json(...)
to_dict(...)
to_json(...)
这里是问题中原始代码的略微修改版本。我已经添加了所需的 @dataclass_json
装饰器和 assert
s,用于从 dict
s 到 Point
和 C
:
的实例的转换
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)
我真的认为 gatopeich 在 答案中提出的概念是解决这个问题的最佳方法。
我修复了他的代码并使之文明化。这是从字典中加载数据类的正确函数:
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 版本开始,这最近(无论如何是两周前)移动到 Production/Stable 状态。
它非常可靠地支持从 typing
模块输入泛型,以及其他小众用例,例如 Union
类型中的数据 classes 和模式化的日期和时间.我个人认为非常有用的其他可有可无的功能,例如自动 键套管 转换(即骆驼到蛇)和隐式 type转换(即带注释的字符串 int
)也已实现。
理想的用法是使用 JSONWizard
Mixin class,它提供了有用的 class 方法,例如:
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
注意:值得注意的是,从技术上讲,您只需要子class主要数据class,即被序列化的模型;如果需要,嵌套数据classes 可以单独保留。
如果完全不需要 class 继承模型,另一种选择是使用导出的辅助函数,例如 fromdict
、asdict
来转换数据 class 实例to/from Python dict
个对象。
3.7 中的标准库可以递归地将数据class 转换为字典(示例来自文档):
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
我正在寻找一种在存在嵌套时将字典转回数据的方法class。像 C(**tmp)
这样的东西只有在数据 class 的字段是简单类型而不是它们本身是数据 class 的情况下才有效。我熟悉 [jsonpickle][1],但是它带有一个突出的安全警告。
编辑:
答案建议了以下库:
- 英安岩
- mashumaro(我用了一段时间,效果很好,但我很快 运行 陷入棘手的极端情况)
- pydantic(工作得很好,优秀的文档和更少的角落案例) [1]: https://jsonpickle.github.io/
下面是 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
。
How can this be adapted to create an output dictionary with the required type-tagging, as mentioned in the comments?
1.添加类型信息
我的尝试涉及创建一个自定义 return 包装器,继承自 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
查看原始代码,只需修改第一个子句即可使用此包装器,因为其他子句仅处理 containers of 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)])
再次如预期。
如果您的目标是从 现有的预定义 数据class 生成 JSON,然后只需编写自定义编码器和解码器挂钩。不要在这里使用 dataclasses.asdict()
,而是在 JSON 中记录 对原始数据 class.
jsonpickle
不安全,因为它存储对 任意 Python 对象的引用,并将数据传递给它们的构造函数。有了这些引用,我可以让 jsonpickle 引用内部 Python 数据结构并随意创建和执行函数、classes 和模块。但这并不意味着您不能不安全地处理此类引用。只需验证您只导入(而不是调用),然后验证该对象是实际数据class类型,然后再使用它。
框架可以变得足够通用,但仍然仅限于 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-style class hints 命名数据 class,并且在加载时验证它仍然是具有相同字段的数据 class。没有对字段的值进行类型检查(因为那是完全不同的鱼缸)。
将它们用作 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
and JSONDecoder
classes 的实例。
除了使用完全限定的模块和 class 名称,您还可以使用单独的注册表来映射允许的类型名称;检查注册表中的编码,并再次检查解码,以确保您在开发时不会忘记注册数据classes。
您可以使用 mashumaro 根据方案从字典创建数据类对象。来自这个库的 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
我是 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
支持以下功能:
- 嵌套结构
- (基本)类型检查
- 可选字段(即 typing.Optional)
- 工会
- collections
- 值转换和转换
- 重新映射字段名称
... 并且经过充分测试 - 100% 代码覆盖率!
要安装英安岩,只需使用 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))
完整代码,包括 to/from 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
给定字段中的拼写错误。
我想建议使用复合模式来解决这个问题, 主要优点是您可以继续向此模式添加 classes 并让他们以同样的方式行事。
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 class 中使用工厂模式来处理其他情况,例如嵌套的字典、元组等,这将节省大量样板文件。
无需额外模块,您可以使用 __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))
基于
我还没有看到提到的一个可能的解决方案是使用dataclasses-json
. This library provides conversions of dataclass
instances to/from JSON, but also to/from dict
(like dacite
and mashumaro
,这在之前的答案中被建议过)。
dataclasses-json
除了 @dataclass
之外,还需要用 @dataclass_json
装饰 类。修饰后的 类 然后得到几个成员函数用于转换 to/from JSON 和 to/from dict
:
from_dict(...)
from_json(...)
to_dict(...)
to_json(...)
这里是问题中原始代码的略微修改版本。我已经添加了所需的 @dataclass_json
装饰器和 assert
s,用于从 dict
s 到 Point
和 C
:
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)
我真的认为 gatopeich 在
我修复了他的代码并使之文明化。这是从字典中加载数据类的正确函数:
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 版本开始,这最近(无论如何是两周前)移动到 Production/Stable 状态。
它非常可靠地支持从 typing
模块输入泛型,以及其他小众用例,例如 Union
类型中的数据 classes 和模式化的日期和时间.我个人认为非常有用的其他可有可无的功能,例如自动 键套管 转换(即骆驼到蛇)和隐式 type转换(即带注释的字符串 int
)也已实现。
理想的用法是使用 JSONWizard
Mixin class,它提供了有用的 class 方法,例如:
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
注意:值得注意的是,从技术上讲,您只需要子class主要数据class,即被序列化的模型;如果需要,嵌套数据classes 可以单独保留。
如果完全不需要 class 继承模型,另一种选择是使用导出的辅助函数,例如 fromdict
、asdict
来转换数据 class 实例to/from Python dict
个对象。