【问题标题】:Python attrs/cattrs serializing a dict using frozen attrs classes as the dict keysPython attrs/cattrs 使用冻结的 attrs 类作为字典键序列化字典
【发布时间】:2021-12-24 08:56:27
【问题描述】:

我想构造和解构一个 attrs 对象,其中包括使用简单 frozen 属性作为 dict 键的 dict 字段。这对于在运行时创建的对象非常有效,但是 freeze 属性无法使使用 cattrs 的 un/structuring 变得容易。

这是一个简单的问题示例:

import attr, cattr

# Simple attr that contains only a single primitive data type.
@attr.s(frozen=True)
class AbstractID:
    _id: Optional[int] = attr.ib()

    def __str__(self) -> str:
        if self._id is not None:
            return f"A{self._id}"
        else:
            return "—"


@attr.s(auto_attribs=True)
class Database:
    storage: dict[AbstractID, str] = {}

# Attempt to unstructure using cattrs
db = Database()
db.storage[AbstractID(1)] = "some data"

cattr.unstructure(db)

>>> TypeError: unhashable type: 'dict'

在导入/导出过程之外,是否有某种方法可以在不使用 int 或 str 作为 dict 键的情况下序列化数据? 我看到 cattrs 提供了自定义序列化过程的钩子,但我不知道如何在非结构化时将 AbstractID 减少为 int,或者如何将其结构化回 AbstractID。

这个可以吗?

【问题讨论】:

  • 那么你想要的最终结果是{'storage': {1: 'some data'}}
  • 是的,如果有办法将其重新构造回 Database.storage{AbstractID(1): "some data"}} 之后,那将是非常合适的。

标签: python json python-attrs


【解决方案1】:

默认方法失败,因为它试图生成:

{"storage": {{"_id": 1}: "some_data"}

而且 Python dicts 不支持其他 dicts 作为键。

由于我们将自定义行为,我们将使用一个单独的转换器实例。我还将使用新的 attrs API,因为它们更干净。以下是您想要执行的操作:

from typing import Optional

from attr import define, frozen, Factory

from cattr import GenConverter


# Simple attr that contains only a single primitive data type.
@frozen
class AbstractID:
    _id: Optional[int]

    def __str__(self) -> str:
        if self._id is not None:
            return f"A{self._id}"
        else:
            return "—"


@define
class Database:
    storage: dict[AbstractID, str] = Factory(dict)


# Attempt to unstructure using cattrs
db = Database()
db.storage[AbstractID(1)] = "some data"

c = GenConverter()
c.register_unstructure_hook(AbstractID, lambda aid: aid._id)
c.register_structure_hook(AbstractID, lambda v, _: AbstractID(v))

print(c.unstructure(db))  # {'storage': {1: 'some data'}}
print(c.structure(c.unstructure(db), Database))  # Database(storage={AbstractID(_id=1): 'some data'})

cattrs 让这些事情变得轻松。

【讨论】:

  • 这是完美的解决方案。顺便说一句,我想我认得你的头像,你是 (c)attrs 的贡献者,写了一篇关于使用 attrs 而不是 pydantic 的文章,对吧?我听从了您的建议,并获得了非常愉快的体验。非常感谢!
  • 是的,就是我。我是 cattrs 的作者 ;)
【解决方案2】:

嗯,你总是可以使用marshmallow 来处理这样的事情。它允许您通过模式完全自定义流程。无论如何,将序列化/反序列化与业务逻辑分开通常是一个好主意。因此,对于您的示例,它可能看起来像这样:

from typing import Any
from marshmallow import Schema, fields, post_dump, pre_load, post_load

class AbstractIdSchema(Schema):
    _id = fields.Integer()

    @pre_load
    def pre_load(self, obj: int, **_: Any) -> dict:
        return {'_id': obj}

    @post_load
    def post_load(self, data: dict, **_: Any) -> AbstractID:
        return AbstractID(id=data['_id'])

    @post_dump
    def post_dump(self, data: dict, **_) -> int:
        return data['_id']

class DatabaseSchema(Schema):
    storage = fields.Dict(
        keys=fields.Nested(AbstractIdSchema()),
        values=fields.String(),
    )

    @post_load
    def post_load(self, data: dict, **_: Any) -> Database:
        return Database(**data)

print(db)
db_schema = DatabaseSchema()
serialized_db = db_schema.dump(db)
print(serialized_db)
deserialized_db = db_schema.load(serialized_db)
print(deserialized_db)

# Prints:
# Database(storage={AbstractID(_id=1): 'some data'})
# {'storage': {1: 'some data'}}
# Database(storage={AbstractID(_id=1): 'some data'})

如果_id 只是简单的id(即与属性相同的init arg),它看起来会更简单一些——那么你可以在post_load 中使用AbstractID(**data)

再说一次,如果你的模型真的那么简单,那可能就有点矫枉过正了。但如果现实更复杂,那么它可能是要走的路。

【讨论】:

  • 当您说“将序列化/反序列化与业务逻辑分开通常是一个好主意”时,这是否意味着我会 attrs一起使用,并且基本上将其用作转储/加载数据的方法,作为中介,还是会替换 attrs?我有点担心性能和内存使用情况,据说 attrs 擅长。
  • 当然。这绝不会取代 attrs - 它只处理序列化
  • 哦,我明白了,所以db_schema.dump(db) 函数接受attrs 对象并对其进行序列化(假设attr 对象符合模式),替换了cattrs 的使用,而不是attrs。使用 cattrs 的解决方案本来是理想的,但如果不是更好的话,它也会同样有效,因为实现可以将旧数据库转换为新布局的转换器似乎很容易,即使进行了非平凡的更改。谢谢!
猜你喜欢
  • 2019-05-24
  • 2021-12-15
  • 1970-01-01
  • 2020-11-10
  • 2015-04-18
  • 2020-08-04
  • 2013-05-04
  • 2021-11-10
  • 1970-01-01
相关资源
最近更新 更多