レシピ

この ページでは、カスタムの un-/structuring メカニズムのレシピ集を提供します。

イニシャライザの切り替え

attrs クラスを構造化する際、cattrs はデフォルトでクラスの __init__ メソッドを使用してオブジェクトをインスタンス化します。 特定の状況では、この動作から逸脱して、代わりに代替のイニシャライザを使用したい場合があります。

たとえば、2D 空間内の点を記述する次の Point クラスを考えてみましょう。このクラスは、代替の作成のために 2 つの classmethod を提供します。

>>> import math
>>> from attrs import define

>>> @define
... class Point:
...     """A point in 2D space."""
...     x: float
...     y: float
...
...     @classmethod
...     def from_tuple(cls, coordinates: tuple[float, float]) -> "Point":
...         """Create a point from a tuple of Cartesian coordinates."""
...         return Point(*coordinates)
...
...     @classmethod
...     def from_polar(cls, radius: float, angle: float) -> "Point":
...         """Create a point from its polar coordinates."""
...         return Point(radius * math.cos(angle), radius * math.sin(angle))

代替イニシャライザの選択

classmethod の 1 つをイニシャライザとして 静的に 設定する簡単な方法は、それぞれの呼び出し可能オブジェクトへの参照を保持する構造化フックを登録することです。

>>> from inspect import signature
>>> from typing import Callable, TypedDict

>>> from cattrs import Converter
>>> from cattrs.dispatch import StructureHook

>>> def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
...     """Create a TypedDict reflecting a callable's signature."""
...     params = {p: t.annotation for p, t in signature(fn).parameters.items()}
...     return TypedDict(f"{fn.__name__}_args", params)
...

>>> def make_initializer_from(fn: Callable, conv: Converter) -> StructureHook:
...     """Return a structuring hook from a given callable."""
...     td = signature_to_typed_dict(fn)
...     td_hook = conv.get_structure_hook(td)
...     return lambda v, _: fn(**td_hook(v, td))

これで、指定された代替表現から Point を簡単に構造化できます。

>>> c = Converter()
>>> c.register_structure_hook(Point, make_initializer_from(Point.from_polar, c))

>>> p0 = Point(1.0, 0.0)
>>> p1 = c.structure({"radius": 1.0, "angle": 0.0}, Point)
>>> assert p0 == p1

イニシャライザ間の動的な切り替え

場合によっては、さらに柔軟性が必要となり、イニシャライザの選択は実行時に行われる必要があり、動的なアプローチが必要になります。 典型的なシナリオは、オブジェクトの構造化が API の背後で行われ、ユーザーがシリアル化文字列で提供したいオブジェクトの表現を指定できるようにする場合です。

このような状況では、次のフックファクトリが目標の達成に役立ちます。

>>> from inspect import signature
>>> from typing import Callable, TypedDict

>>> from cattrs import Converter
>>> from cattrs.dispatch import StructureHook

>>> def signature_to_typed_dict(fn: Callable) -> type[TypedDict]:
...     """Create a TypedDict reflecting a callable's signature."""
...     params = {p: t.annotation for p, t in signature(fn).parameters.items()}
...     return TypedDict(f"{fn.__name__}_args", params)

>>> T = TypeVar("T")
>>> def make_initializer_selection_hook(
...     initializer_key: str,
...     converter: Converter,
... ) -> StructureHook:
...     """Return a structuring hook that dynamically switches between initializers."""
...
...     def select_initializer_hook(specs: dict, cls: type[T]) -> T:
...         """Deserialization with dynamic initializer selection."""
...
...         # If no initializer keyword is specified, use regular __init__
...         if initializer_key not in specs:
...             return converter.structure_attrs_fromdict(specs, cls)
...
...         # Otherwise, call the specified initializer with deserialized arguments
...         specs = specs.copy()
...         initializer_name = specs.pop(initializer_key)
...         initializer = getattr(cls, initializer_name)
...         td = signature_to_typed_dict(initializer)
...         td_hook = converter.get_structure_hook(td)
...         return initializer(**td_hook(specs, td))
...
...     return select_initializer_hook

使用するイニシャライザを決定するキーを指定すると、オブジェクト仕様の一部として classmethod を動的に選択できます。

>>> c = Converter()
>>> c.register_structure_hook(Point, make_initializer_selection_hook("initializer", c))

>>> p0 = Point(1.0, 0.0)
>>> p1 = c.structure({"initializer": "from_polar", "radius": 1.0, "angle": 0.0}, Point)
>>> p2 = c.structure({"initializer": "from_tuple", "coordinates": (1.0, 0.0)}, Point)
>>> assert p0 == p1 == p2