使用 `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 str
和 int
.
在 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})
我想将 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 str
和 int
.
在 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})