在 asdict 或序列化中将属性包含在数据类中的推荐方法是什么?

Ken*_*ama 17 python serialization immutability python-3.x python-dataclasses

请注意,这类似于如何在 asdict 中获取 @property 方法?

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

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
Run Code Online (Sandbox Code Playgroud)

我可以按如下方式序列化数据结构:

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}}
Run Code Online (Sandbox Code Playgroud)

但是,我还想序列化属性 ( @property)。注意我不想将属性转换为字段,__post_init__因为我想保持数据类的冻结。我不想obj.__setattr__在冰冻的田野周围工作。我也不想预先计算类外部属性的值并将它们作为字段传递。

我当前使用的解决方案是显式写出每个对象的序列化方式,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

对于几个级别的嵌套,这是可以管理的,但我希望有一个更通用的解决方案。例如,这是一个(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
Run Code Online (Sandbox Code Playgroud)

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

Kro*_*hka 7

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

Class(**asdict(obj)) == obj  # only for classes w/o nested dataclass attrs
Run Code Online (Sandbox Code Playgroud)

如果你没有找到任何相关的 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__', [])}}
Run Code Online (Sandbox Code Playgroud)

然后,您可以以自定义但简短的方式指定您想要在字典中使用哪些内容:

@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')))
Run Code Online (Sandbox Code Playgroud)

:

{'f2': 'f2', 'f': 'f'}
{'f': 'f'}
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,对“std_asdict”的调用不会输出任何嵌套数据类“@property”。 (2认同)

fox*_*lue 7

如果适用于您的解决方案,您可以在基类上定义属性,并让具体类实现这些属性。这适用于asdict.

from dataclasses import asdict, dataclass, field

@dataclass
class Liquid:
    volume: int
    price: int
    total_cost: int = field(init=False)


class Milk(Liquid):
    volume: int
    price: int

    @property
    def total_cost(self):
        return self.volume * self.price


milk = Milk(10, 3)

print(asdict(milk))
>>> {'volume': 10, 'price': 3, 'total_cost': 30}
Run Code Online (Sandbox Code Playgroud)


mar*_*eau 3

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

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

和您一样,我使用当前的实现dataclasses.asdict()作为指南/模板,因为您要求的基本上只是其定制版本。a 的每个字段的当前值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))
Run Code Online (Sandbox Code Playgroud)

输出:

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))
Run Code Online (Sandbox Code Playgroud)