Каков рекомендуемый способ включения свойств в классы данных в asdict или сериализации?

Обратите внимание, что это похоже на Как получить методы @property в 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__, так как я хотел бы, чтобы класс данных оставался замороженным. Я не хочу использовать 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

Для нескольких уровней вложенности это выполнимо, но я надеюсь на более общее решение. Например, вот (хакерское) решение, которое обезьяно исправляет реализацию _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

Есть ли рекомендуемый способ добиться того, что я пытаюсь сделать?


person Kent Shikama    schedule 10.11.2020    source источник


Ответы (2)


Насколько мне известно, нет рекомендуемого способа их включения.

Вот кое-что, что, кажется, работает и, я думаю, отвечает вашим многочисленным требованиям. Он определяет настраиваемый кодировщик, который вызывает свой собственный метод _asdict(), когда объект является dataclass, вместо того, чтобы обезьяна исправляла (частную) функцию dataclasses._asdict_inner() и инкапсулирует (связывает) код в кодировщике клиента, который его использует.

Как и вы, я использовал текущую реализацию {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "d": [42], "c": 0}

person martineau    schedule 12.11.2020
comment
Спасибо за вклад. Я начинаю убеждаться, что этот специальный метод будет включать в основном копирование _asdict_inner. Я немного разочарован тем, что для такого использования нет поддержки по умолчанию. - person Kent Shikama; 13.11.2020
comment
Я даже не ожидал, что появится стандартный способ справиться с тем, что вы хотите сделать, что довольно необычно, ИМО. Например, можно утверждать, что значение property - это его код. В любом случае, я думаю, вы должны принять мой ответ, даже если суть его в том, что это не рекомендуемый способ, потому что, помимо этого, он также содержит жизнеспособный обходной путь, который не включает исправление обезьяной модуля dataclasses. - person martineau; 13.11.2020
comment
Вы используете return str(obj) в качестве заглушки, потому что обработка list, tuple, dict, deepcopy и т. Д. Сделает ответ слишком длинным? - person Kent Shikama; 13.11.2020
comment
Я удивлен, что вы думаете, что то, что я пытаюсь сделать, довольно необычно. Разве люди не создают dataclass с @property? Или дело в том, что свойства обычно не сериализуются с базовыми данными? - person Kent Shikama; 13.11.2020
comment
return str(obj) - это недоработка, оставшаяся после более ранних экспериментов с кодом. Одна из проблем с ними заключается в том, что они могут содержать вложенные классы данных с проблемами и ограничениями, описанными в исходном коде. Я не думаю, что свойства часто используются в сочетании с классами данных - и когда это будет сделано, можно ожидать, что они будут представлены в возвращаемом словаре как нечто иное, чем то, чем они были на самом деле (если бы они не были игнорируется). - person martineau; 13.11.2020
comment
Я добавил код для обработки пропущенных типов - и не думаю, что это сделало код слишком длинным. Вы можете найти сообщение в блоге Согласование классов данных и Свойства В Python интересно, особенно ближе к концу подраздела Попытка 5, в котором говорится ... потому что классы данных были разработаны как редактируемые контейнеры данных. Если вам действительно нужны поля, доступные только для чтения, вам в первую очередь не следует прибегать к классам данных. - person martineau; 15.11.2020
comment
Думаю, вы также можете найти на YouTube видео выступления Раймонда Хеттингера на конференции PyCon 2018 Классы данных: генератор кода для завершения всех генераторов кода вообще стоит посмотреть - т.е. не конкретно в отношении вашего вопроса о asdict и свойствах. - person martineau; 15.11.2020

Вроде противоречит удобной dataclass особенности:

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

Если вы не найдете подходящего пакета pypi, вы всегда можете добавить двухстрочный файл следующим образом:

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'}
person Kroshka Kartoshka    schedule 10.11.2020
comment
К сожалению, вызов std_asdict не приведет к выводу вложенных классов данных @property. - person Kent Shikama; 11.11.2020
comment
Class(**asdict(obj)) == obj не выполняется ни для одного вложенного класса данных даже при реализации по умолчанию, поскольку он не преобразует внутренние словари автоматически. - person Kent Shikama; 11.11.2020
comment
Это правда. Вы знаете какой-нибудь красивый пакет с функцией fromdict? dataclass-json? - person Kroshka Kartoshka; 11.11.2020