Python attrs/cattrs 使用 frozen attrs 类 作为字典键序列化一个字典
Python attrs/cattrs serializing a dict using frozen attrs classes as the dict keys
我想构造和取消构造一个 attrs
对象,其中包括 dict
字段,这些字段使用简单的 frozen 属性作为字典键。这对于在运行时创建的对象非常有效,但是 frozen 属性无法使 un/structuring with cattrs 变得简单。
这是一个简单的问题示例:
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'
有没有什么方法可以在 import/export 进程之外序列化数据,而不使用 int 或 str 作为 dict 键?
我看到 cattrs 提供了钩子来自定义序列化过程,但我不知道如何在解结构时将 AbstractID 减少为 int,或者如何将其结构化回 AbstractID。
这可以做到吗?
好吧,您可以随时使用 marshmallow 处理此类内容。它允许您通过模式完全自定义流程。无论如何,将 serialization/deserialization 与业务逻辑分开通常是个好主意。因此,对于您的示例,它可能看起来像这样:
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)
。
话又说回来,如果您的模型真的那么简单,那可能就有点矫枉过正了。但如果现实更复杂,那么它可能是可行的方法。
默认方法失败,因为它试图生成:
{"storage": {{"_id": 1}: "some_data"}
并且 Python 字典不支持其他字典作为键。
由于我们将自定义行为,因此我们将使用一个单独的转换器实例。我还将使用新的 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
让这些东西变得简单。
我想构造和取消构造一个 attrs
对象,其中包括 dict
字段,这些字段使用简单的 frozen 属性作为字典键。这对于在运行时创建的对象非常有效,但是 frozen 属性无法使 un/structuring with cattrs 变得简单。
这是一个简单的问题示例:
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'
有没有什么方法可以在 import/export 进程之外序列化数据,而不使用 int 或 str 作为 dict 键? 我看到 cattrs 提供了钩子来自定义序列化过程,但我不知道如何在解结构时将 AbstractID 减少为 int,或者如何将其结构化回 AbstractID。
这可以做到吗?
好吧,您可以随时使用 marshmallow 处理此类内容。它允许您通过模式完全自定义流程。无论如何,将 serialization/deserialization 与业务逻辑分开通常是个好主意。因此,对于您的示例,它可能看起来像这样:
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)
。
话又说回来,如果您的模型真的那么简单,那可能就有点矫枉过正了。但如果现实更复杂,那么它可能是可行的方法。
默认方法失败,因为它试图生成:
{"storage": {{"_id": 1}: "some_data"}
并且 Python 字典不支持其他字典作为键。
由于我们将自定义行为,因此我们将使用一个单独的转换器实例。我还将使用新的 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
让这些东西变得简单。