2

我试图找到一个类似于 java Jackson ObjectMapper的解决方案,它可以将 python 对象序列化/反序列化为 json。并发现

  • cattrs最接近我的需要。但它不能像firstName在 json 中使用但first_name在反序列化对象中那样进行属性映射。

  • attrs-serde可以进行属性映射,但不能进行递归反序列化。

这个问题可以在这个例子中说明,

import attr
import cattr
from attrs_serde import serde

name_path = ["contact", "personal", "Name"]
phone_path = ["contact", "Phone"]


@serde
@attr.s(auto_attribs=True, frozen=True)
class Name:
    first: str
    last: str


@serde
@attr.s(auto_attribs=True, frozen=True)
class Person:
    name: Name = attr.ib(metadata={"to": name_path, "from": name_path})
    phone: str = attr.ib(metadata={"to": phone_path, "from": phone_path})


person_json = {"contact": {"personal": {"Name": {"first": "John", "last": "Smith"}}, "Phone": "555-112233"}}

# XXX: to/from only works on serde
p = Person(name=Name(first="John", last="Smith"), phone="555-112233")
print(p.to_dict())
# {'contact': {'personal': {'Name': {'first': 'John', 'last': 'Smith'}}, 'Phone': '555-112233'}}
p1 = Person.from_dict(person_json)
print(f"p1={p1}")
# p1=Person(name={'first': 'John', 'last': 'Smith'}, phone='555-112233')

# XXX: nested only works on cttrs
person = {"Name": {"First": "John", "Last": "Smith"}, "Phone": "555-112233"}
converter = cattr.Converter()
converter.register_structure_hook(
    Person, lambda d, _: Person(name=converter.structure(d["Name"], Name), phone=d.get("Phone"))
)
converter.register_structure_hook(Name, lambda d, _: Name(first=d["First"], last=d.get("Last")))

p2 = converter.structure(person, Person)
print(p2)
assert p == p2

print(converter.unstructure(p2))
# {'name': {'first': 'John', 'last': 'Smith'}, 'phone': '555-112233'}
# {"contact": {"personal": {"name": "John"}, "phone": "555-112233"}}

使用cattr 的任何更优雅的解决方案?

4

2 回答 2

3

您可以使用驼峰进行大小写转换

import humps

import cattr

class CAttrConverter:

    converter = cattr.Converter()

    def __init__(self):
        """
        structure hook for load
        unstructure hook for dump
        """

    def load(self, params, data_cls, camel_to_snake=True):
        """
        :param params: params, mostly from front end
        :param data_cls:
        :param camel_to_snake: need to convert from camel style to snake style
        """
        if camel_to_snake:
            params = humps.depascalize(params)
        return self.converter.structure(params, data_cls)

    def dump(self, data, snake_to_camel=False):
        """
        :param data:
        :param snake_to_camel: dump as camel case
        """
        result: dict = self.converter.unstructure(data)
        if snake_to_camel:
            result = humps.camelize(result)

        return result
于 2020-10-15T08:20:48.980 回答
2

为将来的人发布此信息。
是的,您可以通过重载转换类方法来实现这一点:

def unstructure_attrs_asdict(self, obj) -> Dict[str, Any]:
def structure_attrs_fromdict(
        self, obj: Mapping[str, Any], cl: Type[T]
    ) -> T:

或者如果你想要元组

def unstructure_attrs_astuple(self, obj) -> Tuple[Any, ...]:
def structure_attrs_fromtuple(
        self, obj: Tuple[Any, ...], cl: Type[T]
    ) -> T:

to使用元数据中的和from字段的简单转换器类。我将把处理嵌套字段留给你想象。

from typing import TypeVar, Dict, Any, Mapping, Type

from cattr import Converter
from cattr._compat import fields

T = TypeVar("T")


class ConverterWithMetaDataOverrides(Converter):
    # Classes to Python primitives.
    def unstructure_attrs_asdict(self, obj) -> Dict[str, Any]:
        """Our version of `attrs.asdict`, so we can call back to us."""
        attrs = fields(obj.__class__)
        dispatch = self._unstructure_func.dispatch
        rv = self._dict_factory()
        for a in attrs:
            name = a.name
            serialize_as = name
            if 'to' in a.metadata:
                serialize_as = a.metadata['to']
            v = getattr(obj, name)
            rv[serialize_as] = dispatch(a.type or v.__class__)(v)
        return rv

    def structure_attrs_fromdict(
            self, obj: Mapping[str, Any], cl: Type[T]
    ) -> T:
        """Instantiate an attrs class from a mapping (dict)."""
        # For public use.

        conv_obj = {}  # Start with a fresh dict, to ignore extra keys.
        dispatch = self._structure_func.dispatch
        for a in fields(cl):  # type: ignore
            # We detect the type by metadata.
            type_ = a.type
            name = a.name
            serialize_from = name
            if 'from' in a.metadata:
                serialize_from = a.metadata['from']
            try:
                val = obj[serialize_from]
            except KeyError:
                continue

            if name[0] == "_":
                name = name[1:]

            conv_obj[name] = (
                dispatch(type_)(val, type_) if type_ is not None else val
            )

        return cl(**conv_obj)  # type: ignore


converter = ConverterWithMetaDataOverrides()

用法:

@attrs(slots=True, frozen=True, auto_attribs=True)
class LevelTwo(object):
    a: str = ib(metadata={'from': 'haha_not_a', 'to': 'haha_not_a'})
    b: str
    c: int


@attrs(slots=True, frozen=True, auto_attribs=True)
class LevelOne(object):
    leveltwo: LevelTwo = ib(metadata={'from': 'level_two', 'to': 'level_two'})


@attrs(slots=True, frozen=True, auto_attribs=True)
class Root(object):
    levelone: LevelOne = ib(metadata={'from': 'levelOne', 'to': 'levelOne'})


converter.structure(converter.unstructure(Root(levelone=LevelOne(leveltwo=LevelTwo(a='here', b='here_again', c=42)))),
                    Root)
>>> converter.unstructure(Root(levelone=LevelOne(leveltwo=LevelTwo(a='here', b='here_again', c=42)))
>>> {'levelOne': {'level_two': {'haha_not_a': 'here', 'b': 'here_again', 'c': 42}}}
于 2021-05-11T00:35:13.940 回答