高度な例

この セクションでは、 cattrs の機能の高度な使用例について説明します。

ファクトリ・フックの使用

この例では、 スネークケースの属性を持つ attrs クラスがあり、それらをキャメルケースとしてアン/ストラクチャしたいと仮定しましょう。

警告

この 問題に対するよりシンプルでより良いアプローチは、 クラス属性をキャメルケースにすることです。ただし、 これはフック・ファクトリと cattrs のコンポジション・ベースの設計の優れた例です。

簡単なデータモデルを次に示します。

@define
class Inner:
    a_snake_case_int: int
    a_snake_case_float: float
    a_snake_case_str: str

@define
class Outer:
    a_snake_case_inner: Inner

最も簡単な方法から始めて、 選択肢を1つずつ検討してみましょう。手動でアン/ストラクチャ・フックを作成します。

コードを手で記述して登録するだけです。

def unstructure_inner(inner):
    return {
        "aSnakeCaseInt": inner.a_snake_case_int,
        "aSnakeCaseFloat": inner.a_snake_case_float,
        "aSnakeCaseStr": inner.a_snake_case_str
    }

>>> converter.register_unstructure_hook(Inner, unstructure_inner)

(冗長になるため、 他のアンストラクチャ・フックと2つのストラクチャ・フックは省略します。)

これで目的は達成できますが、 欠点はすぐに明らかです。大量のコードを自分で記述する必要があり、 労力を浪費し、 メンテナンスの負担が増え、 バグのリスクが高まります。明らかにこれでは不十分です。

コードを記述してコードを記述できるのに、 なぜコードを記述するのでしょうか?この場合、 このコードはすでに記述されています。cattrs には、 まさにこのようなフックを自動的に生成する関数を持つモジュール cattrs.gen が含まれています。これらの関数は、 生成されたフックをカスタマイズするためのパラメータも取ります。

必要な名前変更フックを生成して登録できます。

>>> from cattrs.gen import make_dict_unstructure_fn, override

>>> converter.register_unstructure_hook(
...     Inner,
...      make_dict_unstructure_fn(
...          Inner,
...          converter,
...          a_snake_case_int=override(rename="aSnakeCaseInt"),
...          a_snake_case_float=override(rename="aSnakeCaseFloat"),
...          a_snake_case_str=override(rename="aSnakeCaseStr"),
...      )
...  )

(繰り返しますが、 冗長になるため、 他のフックは省略します。)

これはまだ冗長で手動すぎるため、 さらに自動化しましょう。スネークケースの識別子をキャメルケースに変換する方法が必要なので、 Stack Overflow から1つ取得しましょう。

def to_camel_case(snake_str: str) -> str:
    components = snake_str.split("_")
    return components[0] + "".join(x.title() for x in components[1:])

これを attrs.fields と組み合わせることで、 入力を省くことができます。

from attrs import fields
from cattrs.gen import make_dict_unstructure_fn, override

converter.register_unstructure_hook(
    Inner,
    make_dict_unstructure_fn(
        Inner,
        converter,
        **{a.name: override(rename=to_camel_case(a.name)) for a in fields(Inner)}
    )
)

converter.register_unstructure_hook(
    Outer,
    make_dict_unstructure_fn(
        Outer,
        converter,
        **{a.name: override(rename=to_camel_case(a.name)) for a in fields(Outer)}
    )
)

(冗長になるため、 ストラクチャ・フックは省略します。)

これで何とかなりそうですが、 クラスごとに個別にこれを行う必要があります。最後のステップは、 フックを直接使用する代わりに、 フック・ファクトリを使用することです。

フック・ファクトリは、 フックを返す関数です。また、 通常のアン/ストラクチャ・フックのようにクラスに直接アタッチされるのではなく、 述語を使用して登録されます。述語は、 型が与えられた場合に、 それを処理するかどうかを示すブール値を返す関数です。

attrs クラスすべてに対してフック・ファクトリをトリガーしたいので、 型が attrs クラスであるかどうかを認識するための述語が必要です。幸いなことに、 attrs には attrs.has が付属しています。これはまさにそれです。

最後のステップとして、 これらすべてを2つのフック・ファクトリにまとめることができます。

from attrs import has, fields
from cattrs import Converter
from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn, override

converter = Converter()

def to_camel_case(snake_str: str) -> str:
    components = snake_str.split("_")
    return components[0] + "".join(x.title() for x in components[1:])

def to_camel_case_unstructure(cls):
    return make_dict_unstructure_fn(
        cls,
        converter,
        **{
            a.name: override(rename=to_camel_case(a.name))
            for a in fields(cls)
        }
    )

def to_camel_case_structure(cls):
    return make_dict_structure_fn(
        cls,
        converter,
        **{
            a.name: override(rename=to_camel_case(a.name))
            for a in fields(cls)
        }
    )

converter.register_unstructure_hook_factory(
    has, to_camel_case_unstructure
)
converter.register_structure_hook_factory(
    has, to_camel_case_structure
)

converter インスタンスは、 すべての attrs クラスをキャメルケースにアン/ストラクチャするようになります。この最後の例から何も省略されていません。これは完全です。

フォールバック・キー名の使用

データを構造化するとき、 入力データは、 共通の属性に変換する必要がある複数の形式である場合があります。

データストアが新しいスキーマ・バージョンを作成し、 キーの名前を変更する例を考えてみましょう(つまり、 v1 の {'old_field': 'value1'} が v2 の {'new_field': 'value1'} になる)。同時に、 既存のレコードをV1スキーマのシステムに残します。両方のキーを同じフィールドに変換する必要があります。

ここでは、 rename などの組み込みのカスタマイズは不十分です。cattrs は、 少なくとも同じコンバーターでは、 rename を使用して old_fieldnew_field の両方を単一のフィールドに構造化できません。

両方のフィールドをサポートするために、 デフォルトの cattrs 構造化フックに少し前処理を適用できます。1つのアプローチは、 次のデコレータを作成し、 クラスに適用することです。

from attrs import define
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn

converter = Converter()


def fallback_field(
    converter_arg: Converter,
    old_to_new_field: dict[str, str]
):
    def decorator(cls):
        struct = make_dict_structure_fn(cls, converter_arg)

        def structure(d, cl):
            for k, v in old_to_new_field.items():
                if k in d:
                    d[v] = d[k]

            return struct(d, cl)

        converter_arg.register_structure_hook(cls, structure)

        return cls

    return decorator


@fallback_field(converter, {"old_field": "new_field"})
@define
class MyInternalAttr:
    new_field: str

cattrs は、 両方のキー名をクラスの new_field に構造化するようになります。

converter.structure({"new_field": "foo"}, MyInternalAttr)
converter.structure({"old_field": "foo"}, MyInternalAttr)