Union の扱い

cattrs は、attrs クラスとデータクラスの単純な union を 自動的に 処理できます。 より複雑なケースでは、コンバーターのカスタマイズが必要です (union の処理方法が多数存在するため)。

cattrs には、union の処理を支援するためのオプションの戦略が多数用意されています。

デフォルトの Union 戦略

便宜上、cattrs には、デフォルトの union 構造化戦略が含まれています。これは、やや独断的です。

いくつかの attrs クラスまたはデータクラスの union が与えられた場合、デフォルトの union 戦略は、いくつかの方法でそれを処理しようとします。

まず、Literal フィールドを探します。 union の すべてのメンバー がリテラルフィールドを含む場合、cattrs はそのフィールドに基づいて曖昧さ回避関数を生成します。

from typing import Literal

@define
class ClassA:
    field_one: Literal["one"]

@define
class ClassB:
    field_one: Literal["two"] = "two"

この場合、{"field_one": "one"} を含むペイロードは、ClassA のインスタンスを生成します。

注釈

次のスニペットを使用すると、リテラルフィールドの使用を無効にし、レガシー動作を復元できます。

from functools import partial
from cattrs.disambiguators import is_supported_union

converter.register_structure_hook_factory(
    is_supported_union,
    partial(converter._gen_attrs_union_structure, use_literals=False),
)

適切なフィールドがない場合、戦略はクラスを調べて 一意の必須フィールド を探します。

したがって、ClassAClassB の union が与えられた場合:

@define
class ClassA:
    field_one: str
    field_with_default: str = "a default"

@define
class ClassB:
    field_two: str

この戦略では、ペイロードにキー field_one が含まれている場合は ClassA として処理し、キー field_two が含まれている場合は ClassB として処理する必要があると判断します。 フィールド field_with_default はデフォルト値を持つため、オプションとして扱われるため、考慮されません。

バージョン 23.2.0 で変更: リテラルを使用して曖昧さを解消できるようになりました。

バージョン 24.1.0 で変更: attrs クラスに加えて、データクラスがサポートされるようになりました。

追加のメタデータを使用した Union のアンストラクチャリング

注釈

cattrs には、バージョン 23.1 以降、このユースケースを処理するための タグ付き union 戦略 が付属しています。 以下の例は教育目的のためにここに残されていますが、この戦略を優先する必要があります。

2 つのクラス ClassAClassB の簡単なシナリオを想定してみましょう。どちらのクラスにも明確なフィールドがないため、cattrs で自動的に使用することはできません。

@define
class ClassA:
    a_string: str

@define
class ClassB:
    a_string: str

これらのいずれかをアンストラクチャリングするナイーブなアプローチでは、同一の辞書が生成され、クラスを再構築するのに十分な情報が得られません。

>>> converter.unstructure(ClassA("test"))
{'a_string': 'test'}  # Is this ClassA or ClassB? Who knows!

私たちにできることは、アンストラクチャリングされたデータにいくつかの追加情報が存在することを確認し、その情報を後で構造化するのに役立てることです。

まず、Union[ClassA, ClassB] 型のアンストラクチャーフックを登録します。

>>> converter.register_unstructure_hook(
...     Union[ClassA, ClassB],
...     lambda o: {"_type": type(o).__name__,  **converter.unstructure(o)}
... )
>>> converter.unstructure(ClassA("test"), unstructure_as=Union[ClassA, ClassB])
{'_type': 'ClassA', 'a_string': 'test'}

アンストラクチャリングするときは、unstructure_as パラメーターを指定する必要がありました。そうしないと、cattrs は特別な union フックの代わりに、通常のアンストラクチャリングルールを ClassA に適用しただけです。

アンストラクチャリングされたデータにいくつかの情報が含まれるようになったので、それを利用するためのストラクチャリングフックを作成できます。

>>> converter.register_structure_hook(
...     Union[ClassA, ClassB],
...     lambda o, _: converter.structure(o, ClassA if o["_type"] == "ClassA" else ClassB)
... )
>>> converter.structure({"_type": "ClassA", "a_string": "test"}, Union[ClassA, ClassB])
ClassA(a_string='test')