カスタム (非) 構造化

この セクションでは、 cattrs における非構造化と構造化のプロセスをカスタマイズする方法について説明します。

カスタム (非) 構造化フック

独自の構造化関数と非構造化関数を作成し、Converter.register_structure_hook() および Converter.register_unstructure_hook() を使用して型に登録できます。このアプローチは最も柔軟性がありますが、最も多くのボイラープレートが必要です。

register_structure_hook()register_unstructure_hook() は、内部で Python の singledispatch を使用します。singledispatch は強力で高速ですが、いくつかの制限があります。つまり、issubclass() を使用してチェックを実行しますが、これは多くの Python 型では機能しません。いくつかの例を挙げます:

  • さまざまなジェネリックコレクション ( list[int]listサブクラス ではありません)

  • リテラル ( Literal[1]Literal[1]サブクラス ではありません)

  • ジェネリクス ( MyClass[int]MyClassサブクラス ではありません)

  • プロトコル ( runtime_checkable でない限り)

  • FinalNotRequired などのさまざまな修飾子

  • newtypes および 3.12 型エイリアス

  • typing.Annotated

… その他多数。これらの場合、代わりに述語関数を使用する必要があります。

デコレーターとして使用

register_structure_hook()register_unstructure_hook() は、デコレーター としても使用できます。この方法で使用すると、動作が少し異なります。

register_structure_hook() は、フックの戻り値の型を検査し、その型にフックを登録します。

@converter.register_structure_hook
def my_int_hook(val: Any, _) -> int:
    """This hook will be registered for `int`s."""
    return int(val)

register_unstructure_hook() は、最初の引数の型を検査し、その型にフックを登録します。

from datetime import datetime

@converter.register_unstructure_hook
def my_datetime_hook(val: datetime) -> str:
    """This hook will be registered for `datetime`s."""
    return val.isoformat()

ラムダ、他の場所で生成されたフック、アノテーションのないフック、および型イントロスペクションが機能しない状況を扱う場合は、非デコレーターアプローチが依然として推奨されます。

Added in version 24.1.0.

述語フック

述語 は、型を受け取り、関連付けられたフックが特定の型を処理できるかどうかに応じて、true または false を返す関数です。

register_unstructure_hook_func()register_structure_hook_func() は、非/構造化フックを任意の型にリンクするために使用されます。これらのフックは 述語フック と呼ばれ、非常に強力です。

述語フックは、singledispatch フックの後に評価されます。singledispatch フックと述語フックの両方が存在する場合、singledispatch フックが使用されます。述語フックは、登録の逆順に 1 つずつチェックされ、一致するものが見つかるまで繰り返されます。

次の例は、クラスに属性 ( custom ) が存在するかどうかを確認し、構造化ロジックをオーバーライドする述語を示しています。

>>> class D:
...     custom = True
...     def __init__(self, a):
...         self.a = a
...     def __repr__(self):
...         return f'D(a={self.a})'
...     @classmethod
...     def deserialize(cls, data):
...         return cls(data["a"])

>>> cattrs.register_structure_hook_func(
...     lambda cls: getattr(cls, "custom", False), lambda d, t: t.deserialize(d)
... )

>>> cattrs.structure({'a': 2}, D)
D(a=2)

フックファクトリー

フックファクトリーは、高階述語フックです。つまり、フックを 生成する 関数です。フックファクトリーは、作業の一部を別のより早いステップにオフロードすることにより、非常に最適化されたフックを作成するためによく使用されます。

フックファクトリーは、Converter.register_unstructure_hook_factory() および Converter.register_structure_hook_factory() を使用して登録されます。

フックファクトリーを使用して forbid_extra_keys をすべてのアトリビューツクラスに適用する方法を示す例を次に示します。

>>> from attrs import define, has
>>> from cattrs import Converter
>>> from cattrs.gen import make_dict_structure_fn

>>> c = Converter()

>>> c.register_structure_hook_factory(
...     has,
...     lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)
... )

>>> @define
... class E:
...    an_int: int

>>> c.structure({"an_int": 1, "else": 2}, E)
Traceback (most recent call last):
...
cattrs.errors.ForbiddenExtraKeysError: Extra fields in constructor for E: else

フックファクトリーは、追加の必須パラメーターを公開することにより、現在のコンバーターを受け取ることができます。

フックファクトリーの複雑なユースケースは、Using Factory Hooks で説明されています。

デコレーターとして使用

register_unstructure_hook_factory() および register_structure_hook_factory() は、デコレーターとしても使用できます。

非構造化フックファクトリーを使用して キュー の非構造化を処理する例を次に示します。

>>> from queue import Queue
>>> from typing import get_origin
>>> from cattrs import Converter

>>> c = Converter()

>>> @c.register_unstructure_hook_factory(lambda t: get_origin(t) is Queue)
... def queue_hook_factory(cl: Any, converter: Converter) -> Callable:
...     type_arg = get_args(cl)[0]
...     elem_handler = converter.get_unstructure_hook(type_arg)
...
...     def unstructure_hook(v: Queue) -> list:
...         res = []
...         while not v.empty():
...             res.append(elem_handler(v.get_nowait()))
...         return res
...
...     return unstructure_hook

>>> q = Queue()
>>> q.put(1)
>>> q.put(2)

>>> c.unstructure(q, unstructure_as=Queue[int])
[1, 2]

コレクションのカスタマイズ

cattrs.cols モジュールには、コレクションの処理をカスタマイズするのに役立つ述語とフックファクトリーが含まれています。これらのフックファクトリーは、複雑なカスタマイズを適用するためにラップできます。

利用可能な述語は次のとおりです:

Tip

これらの述語は cattrs 固有ではなく、他のコンテキストでも役立つ場合があります。

>>> from cattrs.cols import is_sequence

>>> is_sequence(list[str])
True

利用可能なフックファクトリーは次のとおりです:

追加の述語とフックファクトリーは、要求に応じて追加されます。

たとえば、デフォルトでは、シーケンスは任意のイテラブルからリストに構造化されます。これは緩すぎる可能性があり、デフォルトのリスト構造化フックファクトリーをラップすることで、追加の検証を適用できます。

from cattrs.cols import is_sequence, list_structure_factory

c = Converter()

@c.register_structure_hook_factory(is_sequence)
def strict_list_hook_factory(type, converter):

    # First, we generate the default hook...
    list_hook = list_structure_factory(type, converter)

    # Then, we wrap it with a function of our own...
    def strict_list_hook(value, type):
        if not isinstance(value, list):
            raise ValueError("Not a list!")
        return list_hook(value, type)

    # And finally, we return our own composite hook.
    return strict_list_hook

これで、すべてのシーケンス構造化がより厳密になります:

>>> c.structure({"a", "b", "c"}, list[str])
Traceback (most recent call last):
    ...
ValueError: Not a list!

Added in version 24.1.0.

名前付きタプルのカスタマイズ

名前付きタプルは、namedtuple_dict_structure_factory および namedtuple_dict_unstructure_factory フックファクトリーを使用して、辞書を使用して非/構造化できます。

すべて の名前付きタプルを辞書に非構造化するには:

>>> from typing import NamedTuple

>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
>>> c = Converter()

>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
<function namedtuple_dict_unstructure_factory at ...>

>>> class MyNamedTuple(NamedTuple):
...     a: int

>>> c.unstructure(MyNamedTuple(1))
{'a': 1}

一部の 名前付きタプルのみを辞書に非/構造化するには、フックファクトリーを登録するときに述語関数を変更します:

>>> c.register_unstructure_hook_factory(
...     lambda t: t is MyNamedTuple,
...     namedtuple_dict_unstructure_factory,
... )
<function namedtuple_dict_unstructure_factory at ...>

cattrs.gen ジェネレーターの使用

cattrs.gen モジュールを使用すると、attrs クラス、データクラス、および型付き辞書を非構造化するための特殊なフックを生成およびコンパイルできます。デフォルトの Converter は、これらの型のいずれかを最初に検出すると、ここで説明する生成関数を使用して、それに対する特殊なフックを生成し、フックを登録して使用します。

これらのフックを事前に生成する理由の 1 つは、cattrs の多くの機構をバイパスし、通常の cattrs よりも大幅に高速化できることです。フックは、より複雑なカスタマイズのための優れた構成要素でもあります。

もう 1 つの理由は、属性ごとに動作をオーバーライドすることです。

現在、オーバーライドは、(タプルとは対照的に) 辞書の非/構造化フックの生成のみをサポートし、omit_if_defaultforbid_extra_keysrename 、および omit をサポートします。

omit_if_default

このオーバーライドは、クラスごとまたは属性ごとに適用できます。生成された非構造化フックは、デフォルト値またはファクトリー値と等しい値の非構造化をスキップします。

>>> from cattrs.gen import make_dict_unstructure_fn, override

>>> @define
... class WithDefault:
...    a: int
...    b: dict = Factory(dict)

>>> c = cattrs.Converter()
>>> c.register_unstructure_hook(WithDefault, make_dict_unstructure_fn(WithDefault, c, b=override(omit_if_default=True)))
>>> c.unstructure(WithDefault(1))
{'a': 1}

属性ごとの値は、クラスごとの値をオーバーライドすることに注意してください。この副作用として、フィールドのサブセットの存在を強制することができます。たとえば、dateTime フィールドとそのファクトリーを持つクラスを考えてみましょう。dateTime フィールドの非構造化をスキップすると、一貫性がなくなり、現在の時刻に基づいてしまいます。したがって、omit_if_default ルールをクラスに適用しますが、dateTime フィールドには適用しません。

注釈

The parameter to `make_dict_unstructure_function` is named ``_cattrs_omit_if_default`` instead of just ``omit_if_default`` to avoid potential collisions with an override for a field named ``omit_if_default``.
>>> from datetime import datetime
>>> from cattrs.gen import make_dict_unstructure_fn, override

>>> @define
... class TestClass:
...     a: Optional[int] = None
...     b: datetime = Factory(datetime.utcnow)

>>> c = cattrs.Converter()
>>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
>>> c.register_unstructure_hook(TestClass, hook)
>>> c.unstructure(TestClass())
{'b': ...}

このオーバーライドは、構造化関数を生成するときには効果がありません。

forbid_extra_keys

デフォルトでは、cattrs は非構造化された入力を寛容に受け入れます。余分なキーが辞書に存在する場合、構造化されたオブジェクトを生成するときに無視されます。場合によっては、より厳密なコントラクトを適用し、不明なキーが存在する場合にエラーを発生させることが望ましい場合があります。特に、フィールドにデフォルト値がある場合、これはタイプミスをキャッチするのに役立ちます。forbid_extra_keys は、make_dict_structure_fn() で構造フックを作成するときに、クラスごとに有効 (または無効) にすることもできます。

>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class TestClass:
...    number: int = 1
>>>
>>> c = cattrs.Converter(forbid_extra_keys=True)
>>> c.structure({"nummber": 2}, TestClass)
Traceback (most recent call last):
...
ForbiddenExtraKeyError: Extra fields in constructor for TestClass: nummber
>>> hook = make_dict_structure_fn(TestClass, c, _cattrs_forbid_extra_keys=False)
>>> c.register_structure_hook(TestClass, hook)
>>> c.structure({"nummber": 2}, TestClass)
TestClass(number=1)

この動作は、クラスまたは Converter のデフォルトにのみ適用でき、非構造化関数を生成するときには効果がありません。

バージョン 23.2.0 で変更: make_dict_structure_fn._cattrs_forbid_extra_keys パラメーターの値は、デフォルトで指定されたコンバーターから取得されるようになりました。

rename

rename オーバーライドを使用すると、cattrs は実際の属性名の代わりに指定された名前を使用します。これは、属性名が Python の予約キーワードである場合に役立ちます。

>>> from pendulum import DateTime
>>> from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn, override

>>> @define
... class ExampleClass:
...     klass: Optional[int]

>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, klass=override(rename="class"))
>>> st_hook = make_dict_structure_fn(ExampleClass, c, klass=override(rename="class"))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.register_structure_hook(ExampleClass, st_hook)
>>> c.unstructure(ExampleClass(1))
{'class': 1}
>>> c.structure({'class': 1}, ExampleClass)
ExampleClass(klass=1)

omit

このオーバーライドは、個々の属性にのみ適用できます。omit オーバーライドを使用すると、構造化関数または非構造化関数を生成するときに、属性が完全にスキップされます。

>>> from cattrs.gen import make_dict_unstructure_fn, override
>>>
>>> @define
... class ExampleClass:
...     an_int: int
>>>
>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, an_int=override(omit=True))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.unstructure(ExampleClass(1))
{}

struct_hook および unstruct_hook

デフォルトでは、ジェネレーターは、個々の属性の型に応じて、生成時にクラスの各属性に適切な非/構造フックを決定します。

このプロセスは、目的の非/構造フックを手動で渡すことによってオーバーライドできます。

>>> from cattrs.gen import make_dict_structure_fn, override

>>> @define
... class ExampleClass:
...     an_int: int

>>> c = cattrs.Converter()
>>> st_hook = make_dict_structure_fn(
...     ExampleClass, c, an_int=override(struct_hook=lambda v, _: v + 1)
... )
>>> c.register_structure_hook(ExampleClass, st_hook)

>>> c.structure({"an_int": 1}, ExampleClass)
ExampleClass(an_int=2)

use_alias

デフォルトでは、フィールドはフィールド名と完全に一致する辞書キーとの間で非/構造化されます。attrs クラスは attrs フィールドエイリアスをサポートしており、特定のフィールドの __init__ パラメーター名をオーバーライドします。_cattrs_use_alias=True で非/構造関数を生成することにより、cattrs はフィールド名の代わりにフィールドエイリアスを非/構造化された辞書キーとして使用します。

>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class AliasClass:
...    number: int = field(default=1, alias="count")
>>>
>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(AliasClass, c, _cattrs_use_alias=True)
>>> c.register_structure_hook(AliasClass, hook)
>>> c.structure({"count": 2}, AliasClass)
AliasClass(number=2)

Added in version 23.2.0.

include_init_false

デフォルトでは、init=False として定義された attrs フィールドは、非/構造化時にスキップされます。_cattrs_include_init_false=True で非/構造関数を生成することにより、すべての init=False フィールドが非/構造化に含まれます。

>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class ClassWithInitFalse:
...    number: int = field(default=1, init=False)
>>>
>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(ClassWithInitFalse, c, _cattrs_include_init_false=True)
>>> c.register_structure_hook(ClassWithInitFalse, hook)
>>> c.structure({"number": 2}, ClassWithInitFalse)
ClassWithInitFalse(number=2)

単一の属性は、omit=False でオーバーライドすることで含めることができます。

>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(ClassWithInitFalse, c, number=override(omit=False))
>>> c.register_structure_hook(ClassWithInitFalse, hook)
>>> c.structure({"number": 2}, ClassWithInitFalse)
ClassWithInitFalse(number=2)

Added in version 23.2.0.