cattrs が必要な理由

Python には、辞書、リスト、タプルなど、強力で使いやすい組み込みの 非構造化 データ型が豊富に用意されています。これらのデータ型は、JSON、MessagePack、CBOR、YAML、TOML などの一般的なシリアライズ形式に簡単に変換できます。

しかし、ビジネスロジック で使用されるデータは、明確に定義されたクラスに 構造化 されるべきです。なぜなら、フィールド名や値のすべての組み合わせがプログラムへの有効な入力となるわけではないからです。データの構造をより信頼できるほど、コードはよりシンプルになり、心配する必要のあるエッジケースは少なくなります。

ネットワーク、ファイルシステム、データベースなどから非構造化データを受け取った場合、cattrs はこのデータを信頼できる構造化データに変換するのに役立ちます。構造化データを他のライブラリが処理できるデータ型に変換する必要がある場合、cattrs はクラスと列挙型を辞書、整数、文字列に変換します。

attrs (そしてある程度は dataclasses) は、データの構造を宣言的に記述するための優れたライブラリですが、意図的にシリアライゼーションライブラリではありません。attrs.asdict(your_instance)YourClass(**data) が、変換プロセスをより細かく制御する必要があるために失敗し始めたとき、cattrs が役に立ちます。

cattrs は、attrs クラスや dataclasses との組み合わせで最高のパフォーマンスを発揮します。これらのライブラリでは、単純な (非) 構造化が、ネストされたデータであっても、シリアライゼーションの詳細でデータモデルを汚染することなく、すぐに利用できます。

>>> from attrs import define
>>> from cattrs import structure, unstructure
>>> @define
... class C:
...     a: int
...     b: list[str]
>>> instance = structure({'a': 1, 'b': ['x', 'y']}, C)
>>> instance
C(a=1, b=['x', 'y'])
>>> unstructure(instance)
{'a': 1, 'b': ['x', 'y']}

重要

構造化と非構造化の詳細がクラス、つまりデータモデルを汚染 しない ことに注意してください。変換を構成する必要がある場合は、データモデル内ではなく、cattrs 自体の中で行われます。

Python には、Web API などに基づいてデータモデルを検証およびシリアライゼーションルールと結合する、一般的な検証ライブラリがあります。私たちはそれは間違ったアプローチだと考えています。検証とシリアライゼーションは、プログラムのエッジに関するものであり、コアに関するものではありません。それらは、ビジネスコードに設計上の圧力をかけるべきではなく、不必要な検証によってコードのパフォーマンスに影響を与えるべきでもありません。大規模な実際のコードベースでは、異なる検証およびシリアライゼーションルールを必要とする複数のソースからのデータも一般的です。

🎶 分離しておきましょう。 🎶

cattrs は、非構造化データを特定の (まだ非構造化された) 形状に 正規化 したい場合に、辞書、リスト、タプルなどの通常の Python コレクション型でも機能します。たとえば、float、int、および文字列のリストを int のタプルに変換するには:

>>> import cattrs

>>> cattrs.structure([1.0, 2, "3"], tuple[int, int, int])
(1, 2, 3)

最後に、attrs クラスを含む、はるかに複雑な例を次に示します。ここでは、cattrs は型アノテーションを解釈して、Enum やネストされたデータ構造を含むデータを正しく構造化および非構造化します。

>>> from enum import unique, Enum
>>> from typing import Sequence
>>> from cattrs import structure, unstructure
>>> from attrs import define, field

>>> @unique
... class CatBreed(Enum):
...     SIAMESE = "siamese"
...     MAINE_COON = "maine_coon"
...     SACRED_BIRMAN = "birman"

>>> @define
... class Cat:
...     breed: CatBreed
...     names: Sequence[str]

>>> @define
... class DogMicrochip:
...     chip_id = field()  # Type annotations are optional, but recommended
...     time_chipped: float = field()

>>> @define
... class Dog:
...     cuteness: int
...     chip: DogMicrochip | None = None

>>> p = unstructure([Dog(cuteness=1, chip=DogMicrochip(chip_id=1, time_chipped=10.0)),
...                  Cat(breed=CatBreed.MAINE_COON, names=('Fluffly', 'Fluffer'))])

>>> p
[{'cuteness': 1, 'chip': {'chip_id': 1, 'time_chipped': 10.0}}, {'breed': 'maine_coon', 'names': ['Fluffly', 'Fluffer']}]
>>> structure(p, list[Dog | Cat])
[Dog(cuteness=1, chip=DogMicrochip(chip_id=1, time_chipped=10.0)), Cat(breed=<CatBreed.MAINE_COON: 'maine_coon'>, names=['Fluffly', 'Fluffer'])]

Tip

非構造化データは、処理するために構造化データに変換する必要がある低レベルの表現であると考え、structure() を使用します。完了したら、データを unstructure() して非構造化形式にし、別のライブラリまたはモジュールに渡します。

特徴

再帰的な非構造化

  • attrs クラスと dataclasses は、attrs.asdict() と同様の方法で辞書に変換されるか、attrs.astuple() と同様の方法でタプルに変換されます。

  • 列挙型のインスタンスは、それらの値に変換されます。

  • 他の型は、変換なしでそのまま使用されます。これには、整数、辞書、リスト、および attrs クラス以外のクラスのインスタンスなどの型が含まれます。

  • 任意の型のカスタムコンバーターは、register_unstructure_hook を使用して登録できます。

再帰的な構造化

非構造化データを、型として指定された仕様に従って、再帰的に構造化データに変換します。次の型がサポートされています:

  • typing.Optional[T] およびその 3.10 以降の形式である T | None

  • list[T]typing.List[T]typing.MutableSequence[T]typing.Sequence[T] はリストに変換されます。

  • tuple および typing.Tuple (両方のバリアント、tuple[T, ...] および tuple[X, Y, Z])。

  • set[T]typing.MutableSet[T]、および typing.Set[T] はセットに変換されます。

  • frozenset[T] および typing.FrozenSet[T] は frozenset に変換されます。

  • dict[K, V]typing.Dict[K, V]typing.MutableMapping[K, V]、および typing.Mapping[K, V] は辞書に変換されます。

  • typing.TypedDict、通常およびジェネリック。

  • typing.NewType

  • PEP 695 型エイリアス (3.12 以降)

  • 単純な属性と通常の __init__[1] を持つ attrs クラス。

  • 通常の __init__ を持つすべての attrs クラスと dataclasses (それらの複雑な属性が型メタデータを持っている場合)。

  • サポートされている attrs クラスの Union (すべてのクラスが一意のフィールドを持っている場合)。

  • 任意のものの Union (それに対する曖昧さ回避関数を提供する場合)。

  • 任意の型のカスタムコンバーターは、register_structure_hook を使用して登録できます。

標準添付

cattrs には、JSON (標準ライブラリ、orjsonUltraJSON)、msgpackcbor2bsonPyYAMLtomlkitmsgspec (現時点では JSON のみをサポート) など、多数のシリアライゼーションライブラリ用に事前構成されたコンバーターが付属しています。

詳細については、cattrs.preconf パッケージ を参照してください。

設計上の決定

cattrs は、いくつかの基本的な設計上の決定に基づいています:

  • 非構造化/構造化ルールはモデルから分離されています。これにより、モデルは非構造化/構造化ルールと一対多の関係を持つことができ、所有しておらず変更できないモデルの非構造化/構造化ルールを作成できます。(cattrs は、use_class_methods 戦略 を使用して、モデルから非構造化/構造化ルールを使用するように構成できます。)

  • 可能な限り発明を控え、代わりに既存の通常の Python を再利用します。たとえば、cattrs には、認可された Python の exceptiongroups が登場するまで、例外をグループ化するためのカスタム例外型はありませんでした。この設計上の決定の副作用として、多くの場合、cattrs の問題を解決しているときに、実際には cattrs を学習するのではなく、Python を学習しています。

  • 推測する誘惑に抵抗してください。問題を解決する方法が 2 つある場合、cattrs は推測を拒否し、ユーザー自身が構成できるようにする必要があります。

愚かな一貫性は、小さな心の鬼です。したがって、これらの決定は破られることもありますが、良い基盤であることが証明されています。

追加のドキュメントと講演