您尝试做的事情有点不明智; attrs 类的全部意义在于提前枚举所有字段。如果您在实例上粘贴任意属性,则必须使用非插槽类,您的辅助函数(如 __repr__ 和 __eq__ 将无法正常工作(额外的属性将被忽略),并且您正确地得出结论 cattrs 无济于事您可以进行类型转换(因为它实际上无法找到类型)。
也就是说,我已经重写了您的示例,将逻辑从类移到转换器中,我觉得它更优雅。
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。
现在,我还有一个反提案。假设我们没有将额外的属性直接粘贴在类上,而是将它们收集到字典中并将该字典粘贴到常规字段中。这是整个示例,已重写:
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})