使用 `cattrs` 将 JSON 结构化为带有额外字段的 `attrs` class?

Structure JSON to an `attrs` class with extra fields using `cattrs`?

我想将 JSON 结构化为 attrs class,允许使用 cattrs 的额外字段。 cattrs 默认情况下将忽略额外字段,如果 forbid_extra_keys=True 传递额外字段时会引发错误。

我想做一些相反的事情:通过允许额外的字段来更改默认行为。为此,我创建了一个 attrs class,但我不确定如何继续使用自定义 cattrs 转换器。这是我目前所拥有的:

import attr
from cattr.preconf.json import make_converter


@attr.s(auto_detect=True)
class ClassWithExtras:
    foo: int

    def __init__(self, **attributes) -> None:
        for field, value in attributes.items():
            if field in self.__attrs_attrs__:
                self.__attrs_init__(field=value)
            else:
                setattr(self, field, value)

converter = make_converter()
converter.register_structure_hook_func(
    lambda cls: issubclass(cls, ClassWithExtras), lambda attribs, cls: cls(**attribs)
)

structured = converter.structure({"foo": "2", "bar": 5}, ClassWithExtras)

我的问题是,因为我们基本上只是在 class 中解压字典,所以类型不正确。即:做类似 structured.foo + structured.bar 的事情会引发错误,我们不能 concatenate/sum strint.

cattrs/attrs 中有没有办法做到这一点?

您的尝试有点不明智; attrs classes 的全部要点是预先枚举所有字段。如果你在实例上粘贴任意属性,你必须使用非插槽 classes,你的辅助函数如 __repr____eq__ 将无法正常工作(额外的属性将被忽略) ,并且正如您正确地得出结论,cattrs 无法帮助您进行类型转换(因为它无处可实际找到类型)。

就是说,我已经重写了您的示例,将逻辑从 class 移到一个转换器中,我觉得这样更优雅。

from typing import Any
from attr import define, fields

from cattr.gen import make_dict_structure_fn
from cattr.preconf.json import make_converter


@define(slots=False)
class ClassWithExtras:
    foo: int


converter = make_converter()


def make_structure(cl):
    # First we generate what cattrs would have used by default.
    default_structure = make_dict_structure_fn(cl, converter)

    # We generate a set of known attribute names to use later.
    attribute_names = {a.name for a in fields(cl)}

    # Now we wrap this in a function of our own making.
    def structure(val: dict[str, Any], _):
        res = default_structure(val)
        # `res` is an instance of `cl` now, so we just stick
        # the missing attributes on it now.
        for k in val.keys() - attribute_names:
            setattr(res, k, val[k])
        return res

    return structure


converter.register_structure_hook_factory(
    lambda cls: issubclass(cls, ClassWithExtras), make_structure
)

structured = converter.structure({"foo": "2", "bar": 5}, ClassWithExtras)
assert structured.foo == 2
assert structured.bar == 5

这基本上完成了您的示例所做的,只是使用 cattrs 而不是 attrs。

现在,我也有一个反建议。比方说,我们不是将额外属性直接粘贴到 class 上,而是将它们收集到字典中并将该字典粘贴到常规字段中。这是重写的整个示例:

from typing import Any

from attr import define, fields

from cattr.gen import make_dict_structure_fn
from cattr.preconf.json import make_converter


@define
class ClassWithExtras:
    foo: int
    extras: dict[str, Any]


converter = make_converter()


def make_structure(cl):
    # First we generate what cattrs would have used by default.
    default_structure = make_dict_structure_fn(cl, converter)

    # We generate a set of known attribute names to use later.
    attribute_names = {a.name for a in fields(cl)}

    # Now we wrap this in a function of our own making.
    def structure(val: dict[str, Any], _):
        val["extras"] = {k: val[k] for k in val.keys() - attribute_names}
        res = default_structure(val)
        return res

    return structure


converter.register_structure_hook_factory(
    lambda cls: issubclass(cls, ClassWithExtras), make_structure
)

structured = converter.structure({"foo": "2", "bar": 5}, ClassWithExtras)
assert structured.foo == 2
assert structured.extras["bar"] == 5

assert structured == ClassWithExtras(2, {"bar": 5})