ストラテジー

cattrs には、un/structuring の動作をカスタマイズするためのいくつかの strategies が付属しています。

ストラテジーは、コンバーターに複雑なカスタマイズを迅速かつ簡単に適用するための、事前パッケージ化された高度なパターンです。

タグ付きユニオン戦略

cattrs.strategies.configure_tagged_union() で見つかりました.

タグ付きユニオン 戦略を使用すると、非構造化表現に追加のフィールド (タグ) を含めることで、クラスのユニオンを un/structuring できます。各タグ値は、ユニオンのメンバーに関連付けられています。

>>> from cattrs.strategies import configure_tagged_union
>>> from cattrs import Converter
>>> converter = Converter()

>>> @define
... class A:
...     a: int

>>> @define
... class B:
...     b: str

>>> configure_tagged_union(A | B, converter)

>>> converter.unstructure(A(1), unstructure_as=A | B)
{'a': 1, '_type': 'A'}

>>> converter.structure({'a': 1, '_type': 'A'}, A | B)
A(a=1)

デフォルトでは、タグフィールド名は _type で、タグ値はユニオンメンバーのクラス名です。フィールド名と値はどちらもオーバーライドできます。

tag_generator パラメーターは、ユニオンのすべてのメンバーで呼び出され、タグ値からユニオンメンバーへのマッピングを生成する、1 つの引数を持つ呼び出し可能です。一般的な tag_generator の使用法を次に示します。

利用可能なタグ情報

推奨される tag_generator

クラスの名前

デフォルトを使用するか、lambda cl: cl.__name__ を使用します

クラス変数 (classvar)

lambda cl: cl.classvar

辞書 (mydict)

mydict.get または mydict.__getitem__

可能な値の列挙型

クラスから列挙値への辞書を作成して使用します

ユニオンのメンバーは、attrs クラスまたは dataclass である必要はありませんが、それらは自動的に機能します。それらは、cattrs が辞書との間で un/structure できるものであれば何でもかまいません。たとえば、登録済みのカスタムフックを持つ型などです。

タグがない場合、または不明な場合に使用されるデフォルトのメンバーを指定できます。これは、下位互換性のある方法で API を進化させるのに役立ちます。クラス A を受け取るエンドポイントを、A をデフォルトとして A | B を受け取るように変更できます (タグを送信しない古いクライアントの場合)。

この戦略は、ユニオンのコンテキストでのみ適用されます。通常の un/structuring フックは変更されません。これは、ユニオンメンバーを複数のユニオンで簡単に再利用できることも意味します。

# Unstructuring as a union.
>>> converter.unstructure(A(1), unstructure_as=A | B)
{'a': 1, '_type': 'A'}

# Unstructuring as just an `A`.
>>> converter.unstructure(A(1))
{'a': 1}

実際のケーススタディ

Apple App Store は サーバーコールバック をサポートしており、Apple は JSON ペイロードをユーザーが選択した URL に送信します。ペイロードは、notificationType フィールドの値に基づいて、約 12 種類の異なるメッセージとして解釈できます。

例を簡単にするために、REFUND イベント用のクラスと、それ以外のすべてのイベント用のクラスの 2 つを定義します。

@define
class Refund:
    originalTransactionId: str

@define
class OtherAppleNotification:
    notificationType: str

AppleNotification = Refund | OtherAppleNotification

次に、タグ付きユニオン 戦略を使用して、コンバーターを準備します。Refund イベントのタグ値は REFUND であり、他のすべてのケースは OtherAppleNotification クラスに処理させることができます。tag_generator パラメーターは呼び出し可能であるため、辞書の get メソッドを指定できます。

>>> from cattrs.strategies import configure_tagged_union

>>> c = Converter()
>>> configure_tagged_union(
...     AppleNotification,
...     c,
...     tag_name="notificationType",
...     tag_generator={Refund: "REFUND"}.get,
...     default=OtherAppleNotification
... )

コンバーターは、Apple 通知の構造化を開始する準備ができました。

>>> payload = {"notificationType": "REFUND", "originalTransactionId": "1"}
>>> notification = c.structure(payload, AppleNotification)

>>> match notification:
...     case Refund(txn_id):
...         print(f"Refund for {txn_id}!")
...     case OtherAppleNotification(not_type):
...         print("Can't handle this yet")
Refund for 1!

Added in version 23.1.0.

サブクラスを含める戦略

cattrs.strategies.include_subclasses() で見つかりました.

サブクラスを含める 戦略を使用すると、基本クラスの un/structuring を、それ自体のインスタンスまたはその子孫の 1 つに対して行うことができます。概念的には、この戦略では、基本クラスの un/structure 操作が要求されるたびに、cattrs メカニズムは、基本クラスとその子孫のユニオンが代わりに要求されたかのように、その操作を置き換えます。

>>> from attrs import define
>>> from cattrs.strategies import include_subclasses
>>> from cattrs import Converter

>>> @define
... class Parent:
...     a: int

>>> @define
... class Child(Parent):
...     b: str

>>> converter = Converter()
>>> include_subclasses(Parent, converter)

>>> converter.unstructure(Child(a=1, b="foo"), unstructure_as=Parent)
{'a': 1, 'b': 'foo'}

>>> converter.structure({'a': 1, 'b': 'foo'}, Parent)
Child(a=1, b='foo')

上記の例では、Child インスタンスを Parent クラスとして非構造化してから構造化するように要求し、どちらの場合も、Child インスタンスの非構造化バージョンと構造化バージョンを正しく取得しました。include_subclasses 戦略を適用しなかった場合、次のようになります。

>>> converter_no_subclasses = Converter()

>>> converter_no_subclasses.unstructure(Child(a=1, b="foo"), unstructure_as=Parent)
{'a': 1}

>>> converter_no_subclasses.structure({'a': 1, 'b': 'foo'}, Parent)
Parent(a=1)

戦略を適用しない場合、非構造化操作と構造化操作の両方で、Parent インスタンスを受け取りました。

注釈

サブクラスの処理がオプトイン機能である主な理由は 2 つあります。

  • パフォーマンス。小さい場合が多く、ほとんどの場合無視できますが、サブクラスの処理にはより多くの関数呼び出しが発生し、パフォーマンスに影響を与えます。

  • カスタマイズ。サブクラスの特定の処理は、状況によって異なる場合があります。特に、ユニオン型を明確にするための普遍的に優れたデフォルトはありません。したがって、決定はユーザーに委ねられます。

警告

適切に機能させるには、include_subclasses 戦略が converter に適用されるときに、すべてのサブクラスを定義する必要があります。サブクラス型が後で定義された場合 (たとえば、継承を使用するプラグインメカニズムのコンテキストで)、それらの後で定義されたサブクラスはサブクラスユニオン型の一部ではなくなり、期待どおりに un/structured されません。

カスタマイズ

前のセクションで示した例では、include_subclasses のデフォルトオプションは、Child クラスに Parent クラスに存在しない属性 (b 属性) があるため、うまく機能します。ユニオンの各タイプの一意のフィールドを見つけることに基づく自動ユニオン型曖昧性除去関数は、意図したとおりに機能します。

場合によっては、より多くの曖昧性除去のカスタマイズが必要になります。たとえば、Child に追加の属性がない場合、または Child の兄弟にも b 属性がある場合、非構造化操作は失敗します。これらの場合、タグ付きユニオン戦略 を定義する 2 つの位置引数 (ユニオン型とコンバーター) の呼び出し可能オブジェクトを include_subclasses 戦略に渡すことができます。configure_tagged_union() はそのまま使用できますが、デフォルトを変更する場合は、標準ライブラリの functools モジュールの partial 関数が役立ちます。


>>> from functools import partial
>>> from attrs import define
>>> from cattrs.strategies import include_subclasses, configure_tagged_union
>>> from cattrs import Converter

>>> @define
... class Parent:
...     a: int

>>> @define
... class Child1(Parent):
...     b: str

>>> @define
... class Child2(Parent):
...     b: int

>>> converter = Converter()
>>> union_strategy = partial(configure_tagged_union, tag_name="type_name")
>>> include_subclasses(Parent, converter, union_strategy=union_strategy)

>>> converter.unstructure(Child1(a=1, b="foo"), unstructure_as=Parent)
{'a': 1, 'b': 'foo', 'type_name': 'Child1'}

>>> converter.structure({'a': 1, 'b': 1, 'type_name': 'Child2'}, Parent)
Child2(a=1, b=1)

利用可能なその他のカスタマイズは次のとおりです (include_subclasses() を参照):

  • subclasses 引数を使用して、ユニオンに参加する必要があるサブクラスの正確なリスト。

  • 属性の名前変更など、属性の un/structuring のカスタマイズを許可する属性のオーバーライド。

両方のカスタマイズを含む例を次に示します。


>>> from attrs import define
>>> from cattrs.strategies import include_subclasses
>>> from cattrs import Converter, override

>>> @define
... class Parent:
...     a: int

>>> @define
... class Child(Parent):
...     b: str

>>> converter = Converter()
>>> include_subclasses(
...     Parent,
...     converter,
...     subclasses=(Parent, Child),
...     overrides={"b": override(rename="c")}
... )

>>> converter.unstructure(Child(a=1, b="foo"), unstructure_as=Parent)
{'a': 1, 'c': 'foo'}

>>> converter.structure({'a': 1, 'c': 'foo'}, Parent)
Child(a=1, b='foo')

Added in version 23.1.0.

クラス固有の構造化および非構造化メソッドの使用

cattrs.strategies.use_class_methods() で見つかりました.

この戦略により、モデル自体で un/structuring ロジックを使用できます。構造化と非構造化の両方に適用できます (同時にも)。

クラスが (un)structuring に特別な処理を必要とする場合は、専用の (un)structuring メソッドを追加できます。

>>> from attrs import define
>>> from cattrs import Converter
>>> from cattrs.strategies import use_class_methods

>>> @define
... class MyClass:
...     a: int
...
...     @classmethod
...     def _structure(cls, data: dict):
...         return cls(data["b"] + 1)  # expecting "b", not "a"
...
...     def _unstructure(self):
...         return {"c": self.a - 1}  # unstructuring as "c", not "a"

>>> converter = Converter()
>>> use_class_methods(converter, "_structure", "_unstructure")
>>> print(converter.structure({"b": 42}, MyClass))
MyClass(a=43)
>>> print(converter.unstructure(MyClass(42)))
{'c': 41}

_structure または _unstructure メソッドを持たないクラスは、それぞれ構造化または非構造化にデフォルトの戦略を使用します。他の名前を自由に使用してください。この戦略は、複数回適用できます (異なるメソッド名で)。

ネストされたオブジェクトを (un)構造化する場合は、(un)structuring メソッドにコンバーターパラメーターを追加するだけで、そこにコンバーターが表示されます。

>>> @define
... class Nested:
...     m: MyClass
...
...     @classmethod
...     def _structure(cls, data: dict, conv):
...         return cls(conv.structure(data["n"], MyClass))
...
...     def _unstructure(self, conv):
...         return {"n": conv.unstructure(self.m)}

>>> print(converter.structure({"n": {"b": 42}}, Nested))
Nested(m=MyClass(a=43))
>>> print(converter.unstructure(Nested(MyClass(42))))
{'n': {'c': 41}}

Added in version 23.2.0.

ユニオンパススルー

cattrs.strategies.configure_union_passthrough() で見つかりました.

ユニオンパススルー 戦略により、Converter は、指定された型のユニオンとサブユニオンを構造化できます。

cattrs の非常に一般的なユースケースは、JSONmsgpack などの他のシリアル化ライブラリによって作成されたデータを処理することです。これらのライブラリは、形式に固有のユニオンの値を直接生成できます。たとえば、すべての JSON ライブラリは、数値、ブール値、文字列、および null 値を区別できます。これは、これらの値がワイヤ形式で異なる方法で表されるためです。この戦略により、cattrs はこれらの値の作成を基になるライブラリにオフロードし、最終的な値を検証するだけで済みます。したがって、cattrs 構成済みの JSON コンバーターは、次のタイプを処理できます。

  • bool | int | float | str | None

JSON の例を続けると、この戦略により、これらの値のユニオンのサブセットを構造化することもできます。したがって、サポートされているサブセットユニオンの例を次に示します。

  • bool | int

  • int | str

  • int | float | str

この戦略は、サポートされている型の 1 つ以上の リテラル を含む型もサポートしています。例:

  • Literal["admin", "user"] | int

  • Literal[True] | str | int | float

この戦略は、これらの型の NewTypes もサポートしています。例:

>>> from typing import NewType

>>> UserId = NewType("UserId", int)

>>> converter.loads("12", UserId)
12

サポートされていない型を含むユニオンは、少なくとも 1 つのユニオン型が戦略でサポートされている場合に処理できます。サポートされているユニオン型は、残りの部分 (スピルオーバー と呼ばれます) がコンバーターに再び渡される前にチェックされます。

たとえば、AB が任意の attrs クラスである場合、ユニオン Literal[10] | A | B は JSON コンバーターで直接処理できません。ただし、この戦略は、構造化されている値が Literal[10] と一致するかどうかを確認し (この型はサポートされているため)、一致しない場合は、A | B として構造化するためにコンバーターに返します (別の戦略で処理できます)。

この戦略は、構造化時に O(1) で実行するように設計されています。ユニオンのサイズとユニオンメンバーの順序には依存しません。

この戦略は、次の構成済みコンバーターに事前に適用されています。

Added in version 23.2.0.