来自嵌套字典的 Python 数据类

新手上路,请多包涵

3.7 中的标准库可以递归地将数据类转换为字典(文档中的示例):

 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(效果很好,优秀的文档和更少的角落案例)

原文由 mbatchkarov 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 904
2 个回答

下面是 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 的 _容器_:

 # 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)])

再次如预期。

原文由 meowgoesthedog 发布,翻译遵循 CC BY-SA 4.0 许可协议

我是 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)
  • 工会
  • 收藏品
  • 价值铸造与转型
  • 字段名称的重新映射

…而且它经过了很好的测试——100% 的代码覆盖率!

要安装英安岩,只需使用 pip(或 pipenv):

 $ pip install dacite

原文由 Konrad Hałas 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题