【问题标题】:What is the recommended way to include properties in dataclasses in asdict or serialization?在 asdict 或序列化的数据类中包含属性的推荐方法是什么?
【发布时间】:2021-02-22 22:10:56
【问题描述】:

注意这类似于How to get @property methods in asdict?

我有一个(冻结的)嵌套数据结构,如下所示。定义了一些(纯粹)依赖于字段的属性。

import copy
import dataclasses
import json
from dataclasses import dataclass

@dataclass(frozen=True)
class Bar:
    x: int
    y: int

    @property
    def z(self):
        return self.x + self.y

@dataclass(frozen=True)
class Foo:
    a: int
    b: Bar

    @property
    def c(self):
        return self.a + self.b.x - self.b.y

我可以将数据结构序列化如下:

class CustomEncoder(json.JSONEncoder):
    def default(self, o):
        if dataclasses and dataclasses.is_dataclass(o):
            return dataclasses.asdict(o)
        return json.JSONEncoder.default(self, o)

foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder))

# Outputs {"a": 1, "b": {"x": 2, "y": 3}}

但是,我还想序列化属性 (@property)。注意我不想使用__post_init__ 将属性转换为字段,因为我想保持数据类的冻结。 I do not want to use obj.__setattr__ to work around the frozen fields. 我也不想预先计算类外属性的值并将它们作为字段传递。

我目前使用的解决方案是明确写出每个对象如何序列化如下:

class CustomEncoder2(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Foo):
            return {
                "a": o.a,
                "b": o.b,
                "c": o.c
            }
        elif isinstance(o, Bar):
            return {
                "x": o.x,
                "y": o.y,
                "z": o.z
            }
        return json.JSONEncoder.default(self, o)

foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder2))

# Outputs {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "c": 0} as desired

对于几级嵌套,这是可以管理的,但我希望有一个更通用的解决方案。例如,这是一个(hacky)解决方案,它对数据类库中的 _asdict_inner 实现进行猴子补丁。

def custom_asdict_inner(obj, dict_factory):
    if dataclasses._is_dataclass_instance(obj):
        result = []
        for f in dataclasses.fields(obj):
            value = custom_asdict_inner(getattr(obj, f.name), dict_factory)
            result.append((f.name, value))
        # Inject this one-line change
        result += [(prop, custom_asdict_inner(getattr(obj, prop), dict_factory)) for prop in dir(obj) if not prop.startswith('__')]
        return dict_factory(result)
    elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
        return type(obj)(*[custom_asdict_inner(v, dict_factory) for v in obj])
    elif isinstance(obj, (list, tuple)):
        return type(obj)(custom_asdict_inner(v, dict_factory) for v in obj)
    elif isinstance(obj, dict):
        return type(obj)((custom_asdict_inner(k, dict_factory),
                          custom_asdict_inner(v, dict_factory))
                         for k, v in obj.items())
    else:
        return copy.deepcopy(obj)

dataclasses._asdict_inner = custom_asdict_inner

class CustomEncoder3(json.JSONEncoder):
    def default(self, o):
        if dataclasses and dataclasses.is_dataclass(o):
            return dataclasses.asdict(o)
        return json.JSONEncoder.default(self, o)

foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder3))

# Outputs {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "c": 0} as desired

有没有推荐的方法来实现我想要做的事情?

【问题讨论】:

    标签: python python-3.x serialization immutability python-dataclasses


    【解决方案1】:

    这似乎与方便的dataclass 功能相矛盾:

    Class(**asdict(obj)) == obj  # only for classes w/o nested dataclass attrs
    

    如果您没有找到任何相关的 pypi 包,您可以随时添加这样的 2-liner:

    from dataclasses import asdict as std_asdict
    
    def asdict(obj):
        return {**std_asdict(obj),
                **{a: getattr(obj, a) for a in getattr(obj, '__add_to_dict__', [])}}
    

    然后您可以以自定义但简短的方式指定您想要在 dicts 中的哪些:

    @dataclass
    class A:
        f: str
        __add_to_dict__ = ['f2']
    
        @property
        def f2(self):
            return self.f + '2'
    
    
    
    @dataclass
    class B:
        f: str
    
    print(asdict(A('f')))
    print(asdict(B('f')))
    

    {'f2': 'f2', 'f': 'f'}
    {'f': 'f'}
    

    【讨论】:

    • 不幸的是,对std_asdict 的调用不会输出任何嵌套数据类'@property's。
    • Class(**asdict(obj)) == obj 不适用于任何嵌套数据类,即使使用默认实现也是如此,因为它不会自动转换内部字典。
    • 确实如此。你知道任何带有fromdict 功能的好包吗? dataclass-json?
    • @KroshkaKartoshka 我知道我迟到了,但请尝试检查 marshmallow-dataclass,我正在使用它将字典反序列化到我的数据类中(也适用于嵌套类)
    【解决方案2】:

    据我所知,没有“推荐”的方式来包含它们。

    这似乎可行,我认为可以满足您的众多要求。它定义了一个自定义编码器,当对象是dataclass 时调用它自己的_asdict() 方法,而不是猴子修补(私有)dataclasses._asdict_inner() 函数和encapsulates(捆绑)客户编码器中使用的代码。

    和您一样,我使用 dataclasses.asdict() 的当前实现作为指南/模板,因为您所要求的基本上只是它的定制版本。 property 的每个字段的当前值是通过调用其__get__ 方法获得的。

    import copy
    import dataclasses
    from dataclasses import dataclass, field
    import json
    import re
    from typing import List
    
    class MyCustomEncoder(json.JSONEncoder):
        is_special = re.compile(r'^__[^\d\W]\w*__\Z', re.UNICODE)  # Dunder name.
    
        def default(self, obj):
            return self._asdict(obj)
    
        def _asdict(self, obj, *, dict_factory=dict):
            if not dataclasses.is_dataclass(obj):
                raise TypeError("_asdict() should only be called on dataclass instances")
            return self._asdict_inner(obj, dict_factory)
    
        def _asdict_inner(self, obj, dict_factory):
            if dataclasses.is_dataclass(obj):
                result = []
                # Get values of its fields (recursively).
                for f in dataclasses.fields(obj):
                    value = self._asdict_inner(getattr(obj, f.name), dict_factory)
                    result.append((f.name, value))
                # Add values of non-special attributes which are properties.
                is_special = self.is_special.match  # Local var to speed access.
                for name, attr in vars(type(obj)).items():
                    if not is_special(name) and isinstance(attr, property):
                        result.append((name, attr.__get__(obj)))  # Get property's value.
                return dict_factory(result)
            elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
                return type(obj)(*[self._asdict_inner(v, dict_factory) for v in obj])
            elif isinstance(obj, (list, tuple)):
                return type(obj)(self._asdict_inner(v, dict_factory) for v in obj)
            elif isinstance(obj, dict):
                return type(obj)((self._asdict_inner(k, dict_factory),
                                  self._asdict_inner(v, dict_factory)) for k, v in obj.items())
            else:
                return copy.deepcopy(obj)
    
    
    if __name__ == '__main__':
    
        @dataclass(frozen=True)
        class Bar():
            x: int
            y: int
    
            @property
            def z(self):
                return self.x + self.y
    
    
        @dataclass(frozen=True)
        class Foo():
            a: int
            b: Bar
    
            @property
            def c(self):
                return self.a + self.b.x - self.b.y
    
            # Added for testing.
            d: List = field(default_factory=lambda: [42])  # Field with default value.
    
    
        foo = Foo(1, Bar(2,3))
        print(json.dumps(foo, cls=MyCustomEncoder))
    

    输出:

    {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "d": [42], "c": 0}
    

    【讨论】:

    • 感谢您的意见。我开始确信这种自定义方法主要涉及复制_asdict_inner。我有点失望,没有对这种使用的默认支持。
    • 我什至没想到会有一种标准的方法来处理你想做的事情,这在 IMO 中是相当不寻常的。例如,有人可能会争辩说,property 的“价值”就是它的代码。无论如何,我认为你应该接受我的回答,即使它的要点是“没有推荐的方法”,因为除此之外它还包含一个可行的解决方法,不涉及猴子修补 dataclasses 模块。跨度>
    • 您是否使用return str(obj) 作为存根,因为处理listtupledict、deepcopy 等会使答案过长?
    • 我添加了代码来处理省略的类型——我认为它不会使代码太长。您可能会发现博客文章 Reconciling Dataclasses And Properties In Python 很有趣,尤其是在 Attempt 5 小节的末尾附近,它说“...因为 dataclasses 被设计为可编辑的数据容器。如果你真的需要只读字段,你不应该首先求助于数据类。”
    • 我想您也可能会发现 Raymond Hettinger 的 PyCon 2018 演讲 Dataclasses: The code generator to end all code generators 的 youtube 视频通常值得一看 - 即不是专门针对您关于 asdict 和属性的问题。
    猜你喜欢
    • 1970-01-01
    • 2020-05-04
    • 1970-01-01
    • 2010-11-16
    • 2011-05-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-23
    相关资源
    最近更新 更多