【问题标题】:Make the Python json encoder support Python's new dataclasses使 Python json 编码器支持 Python 的新数据类
【发布时间】:2018-12-19 13:40:23
【问题描述】:

从 Python 3.7 开始,有一种叫做数据类的东西:

from dataclasses import dataclass

@dataclass
class Foo:
    x: str

但是,以下失败:

>>> import json
>>> foo = Foo(x="bar")
>>> json.dumps(foo)
TypeError: Object of type Foo is not JSON serializable

如何使json.dumps()Foo 的实例编码为json objects

【问题讨论】:

    标签: python python-dataclasses


    【解决方案1】:

    就像您可以为 datetime objects 或小数添加对 JSON 编码器的支持一样,您也可以提供自定义编码器子类来序列化数据类:

    import dataclasses, json
    
    class EnhancedJSONEncoder(json.JSONEncoder):
            def default(self, o):
                if dataclasses.is_dataclass(o):
                    return dataclasses.asdict(o)
                return super().default(o)
    
    json.dumps(foo, cls=EnhancedJSONEncoder)
    

    【讨论】:

    • 重要的是要注意,对于一个名为 Foo 的数据类和一个实例 foo_instance = Foo(...)dataclasses.is_dataclass(Foo)dataclasses.is_dataclass(foo_instance) 评估为 True 导致 TypeError 用于 @987654330 @ if o 是数据类本身,而不是它的实例。
    • 嵌套怎么样 - 例如序列化d={ 'a': Foo(1,2), 'b': Foo(3,4)}
    • 你如何用这个处理 ndarray 类型的对象?
    • 使用这样的 json 编码器可以很好地处理嵌套 - json 序列化程序将使用它递归地转换子元素。
    • 至于处理 ndarray,像 if isinstance(obj, np.ndarray) and obj.ndim == 1: return obj.tolist() 这样的东西就可以了。虽然请记住,虽然 python 会愉快地序列化 NaNInfinity 之类的东西,但根据 json 规范,这是不合法的,许多解码器会拒绝它,因此可能建议在编码器中处理这些内容。
    【解决方案2】:

    你不能只使用dataclasses.asdict() 函数来转换数据类吗 听写?比如:

    >>> @dataclass
    ... class Foo:
    ...     a: int
    ...     b: int
    ...     
    >>> x = Foo(1,2)
    >>> json.dumps(dataclasses.asdict(x))
    '{"a": 1, "b": 2}'
    

    【讨论】:

    • 数据类可能是大型结构的深层嵌套部分。通过使用自定义编码器,您可以将json.dumps({"obj": [something_that_may_or_may_not_contain_a_dataclass]}) 设为
    • asdict() 将正确处理所有嵌套的数据类,因此,我们得到通常加载到 jison 字符串中的嵌套字典(但是!例如,在加载到字符串之前,必须另外处理 datetime 类型)
    【解决方案3】:

    获取 JSON 化数据类实例的方式

    有几个选项可以实现该目标,每个选项的选择都意味着分析哪种方法最适合您的需求:

    Standart library: dataclass.asdict

    import dataclasses
    import json
    
    
    @dataclass.dataclass
    class Foo:
        x: str
    
    foo = Foo(x='1')
    json_foo = json.dumps(dataclasses.asdict(foo)) # '{"x": "1"}'
    

    将其取回数据类实例并非易事,因此您可能希望访问该答案https://stackoverflow.com/a/53498623/2067976

    Marshmallow Dataclass

    from dataclasses import field
    from marshmallow_dataclass import dataclass
    
    
    @dataclass
    class Foo:
        x: int = field(metadata={"required": True})
    
    foo = Foo(x='1') # Foo(x='1')
    json_foo = foo.Schema().dumps(foo) # '{"x": "1"}'
    
    # Back to class instance.
    Foo.Schema().loads(json_foo) # Foo(x=1)
    

    作为marshmallow_dataclass 的奖励,您可以在字段本身上使用验证,当有人使用该模式从 json 反序列化对象时将使用该验证。

    Dataclasses Json

    from dataclasses import dataclass
    from dataclasses_json import dataclass_json
    
    
    @dataclass_json
    @dataclass
    class Foo:
        x: int
    
    foo = Foo(x='1')
    json_foo = foo.to_json() # Foo(x='1')
    # Back to class instance
    Foo.from_json(json_foo) # Foo(x='1')
    

    此外,除此之外,marshmallow 数据类为您进行了类型转换,而 dataclassses-json(ver.: 0.5.1) 忽略了这一点。

    Write Custom Encoder

    遵循接受的 Miracle2k 答案并重用自定义 json 编码器。

    【讨论】:

    • 感谢marshmallow_dataclass,它确实是通过验证(甚至是 YAML)从 JSON 获取真实对象的好方法。
    • @mickours 你欢迎 :) 顺便说一句,我没有提到它也适用于 python36(引擎盖下有 dataclass backport)
    • c# + Newtonsoft 多年来,dataclasses_json 是最“自然”的——谢谢!
    • 如果您有兴趣,我也会看看dataclass-wizard。它与dataclasses-json 非常相似,并且效率稍高一些。它还在大多数基本情况下执行类型转换。免责声明:我是这个库的创建者。
    【解决方案4】:

    如果您同意为此使用库,您可以使用dataclasses-json。这是一个例子:

    from dataclasses import dataclass
    
    from dataclasses_json import dataclass_json
    
    
    @dataclass_json
    @dataclass
    class Foo:
        x: str
    
    
    foo = Foo(x="some-string")
    foo_json = foo.to_json()
    

    它还支持嵌入式数据类 - 如果您的数据类有一个类型为另一个数据类的字段 - 如果所有涉及的数据类都具有 @dataclass_json 装饰器。

    【讨论】:

    • 我用嵌入式数据类试过这个,但它不起作用
    【解决方案5】:

    我建议使用to_json() 方法为您的数据类创建一个父类:

    import json
    from dataclasses import dataclass, asdict
    
    @dataclass
    class Dataclass:
        def to_json(self) -> str:
            return json.dumps(asdict(self))
    
    @dataclass
    class YourDataclass(Dataclass):
        a: int
        b: int
    
    x = YourDataclass(a=1, b=2)
    x.to_json()  # '{"a": 1, "b": 2}'
    

    如果您要向所有数据类添加其他功能,这将特别有用。

    【讨论】:

      【解决方案6】:

      使用字典解包可以找到更简单的答案on Reddit

      >>> from dataclasses import dataclass
      >>> @dataclass
      ... class MyData:
      ...   prop1: int
      ...   prop2: str
      ...   prop3: int
      ...
      >>> d = {'prop1': 5, 'prop2': 'hi', 'prop3': 100}
      >>> my_data = MyData(**d)
      >>> my_data
      MyData(prop1=5, prop2='hi', prop3=100)
      

      【讨论】:

      • 这不支持嵌套数据类。
      • 如果 d 来自 json 对象,这可能会引入错误。我们经常假设在payload中添加一个字段并不是一个破坏性的变化,但是在这里,如果你在d中添加一个字段,它会因为“got an unexpected keyword argument 'prop4'”而中断
      • 就原题而言,这是最简单的正确答案。
      【解决方案7】:

      编码dataclassSimpleNamespace 对象的最简单方法是向json.dumps() 提供默认函数,该函数会为无法以其他方式序列化的对象调用,并返回对象__dict__

      json.dumps(foo, default=lambda o: o.__dict__)
      

      【讨论】:

      • 这是一个好主意,通常应该适用于序列化具有简单类型的嵌套模型。我猜这不支持的唯一情况是复杂的 Python 类型,如 Enum 或 datetime,或者在 Union 类型中有数据类的边缘情况,如 A | B。尽管如此,这种方法应该适用于一般的简单案例,如原始问题中所述。
      【解决方案8】:

      您还可以在类中实现asdictjson.dumps 方法。在这种情况下,无需将 json.dumps 导入项目的其他部分:

      
      from typing import List
      from dataclasses import dataclass, asdict, field
      from json import dumps
      
      
      @dataclass
      class TestDataClass:
          """
          Data Class for TestDataClass
          """
          id: int
          name: str
          tested: bool = False
          test_list: List[str] = field(default_factory=list)
      
          @property
          def __dict__(self):
              """
              get a python dictionary
              """
              return asdict(self)
      
          @property
          def json(self):
              """
              get the json formated string
              """
              return dumps(self.__dict__)
      
      
      test_object_1 = TestDataClass(id=1, name="Hi")
      print(test_object_1.__dict__)
      print(test_object_1.json)
      
      

      输出:

      {'id': 1, 'name': 'Hi', 'tested': False, 'test_list': []}
      {"id": 1, "name": "Hi", "tested": false, "test_list": []}
      

      你也可以创建一个父类来继承方法:

      from typing import List
      from dataclasses import dataclass, asdict, field
      from json import dumps
      
      
      @dataclass
      class SuperTestDataClass:
      
          @property
          def __dict__(self):
              """
              get a python dictionary
              """
              return asdict(self)
      
          @property
          def json(self):
              """
              get the json formated string
              """
              return dumps(self.__dict__)
      
      
      @dataclass
      class TestDataClass(SuperTestDataClass):
          """
          Data Class for TestDataClass
          """
          id: int
          name: str
          tested: bool = False
          test_list: List[str] = field(default_factory=list)
      
      
      test_object_1 = TestDataClass(id=1, name="Hi")
      print(test_object_1.__dict__)
      print(test_object_1.json)
      
      
      

      【讨论】:

        【解决方案9】:

        好的,这就是我遇到类似情况时所做的。

        1. 创建一个自定义字典工厂,将嵌套数据类转换为字典。

          def myfactory(数据): return dict(x for x in data if x[1] is not None)

        2. 如果 foo 是您的 @dataclass,那么只需提供您的字典工厂以使用“myfactory()”方法:

          fooDict = asdict(foo, dict_factory=myfactory)

        3. 将 fooDict 转换为 json

          fooJson = json.dumps(fooDict)

        这应该可以!!

        【讨论】:

          【解决方案10】:

          dataclass-wizard 是一个适合您的现代选项。它支持复杂类型,例如日期和时间、typing 模块中的大多数泛型,以及嵌套数据类结构。

          PEP 585604 中引入的“新样式”注释可以通过 __future__ 导入移植回 Python 3.7,如下所示。

          from __future__ import annotations  # This can be removed in Python 3.10
          from dataclasses import dataclass, field
          from dataclass_wizard import JSONWizard
          
          
          @dataclass
          class MyClass(JSONWizard):
              my_str: str | None
              is_active_tuple: tuple[bool, ...]
              list_of_int: list[int] = field(default_factory=list)
          
          
          string = """
          {
            "my_str": 20,
            "ListOfInt": ["1", "2", 3],
            "isActiveTuple": ["true", false, 1]
          }
          """
          
          instance = MyClass.from_json(string)
          print(repr(instance))
          # MyClass(my_str='20', is_active_tuple=(True, False, True), list_of_int=[1, 2, 3])
          
          print(instance.to_json())
          # '{"myStr": "20", "isActiveTuple": [true, false, true], "listOfInt": [1, 2, 3]}'
          
          # True
          assert instance == MyClass.from_json(instance.to_json())
          

          您可以使用pip 安装数据类向导:

          $ pip install dataclass-wizard
          

          一些背景信息:

          对于序列化,它使用dataclasses.asdict 的略微修改(更高效)的实现。在将 JSON 反序列化为数据类实例时,它第一次迭代数据类字段并为每个带注释的类型生成一个解析器,这使得反序列化过程多次运行时效率更高。

          免责声明:我是这个库的创建者(和维护者)。

          【讨论】:

            猜你喜欢
            • 2012-08-20
            • 2013-03-31
            • 2017-10-16
            • 1970-01-01
            • 1970-01-01
            • 2023-03-12
            • 2014-01-31
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多