Pythonのdataclassにおけるmetadata


TL;DR

dataclassのmetadataを使うと、メンバ変数にメタ情報を付与できる。
メタ情報を付与することで、実装を簡略化できるケースがある。
※metadataはサードパーティー製用なので本来の意図とは違う可能性あり

dataclasses(dataclass)とは

Python3.7から追加されたデータを格納するクラスを簡単に定義できる機能を提供するモジュールです。

こんな感じ

from dataclasses import dataclass

@dataclass
class Company:
    """
    会社クラス
    """
    name: str
    address: str
    average_age: int
    description: str

if __name__ == "__main__":
    company = Company(name="創屋", address="石川県白山市", average_age=30, description="創屋はAIの会社です")

metadataとは

以下公式より参照

metadata: これはマッピングあるいは None に設定できます。 None は空の辞書として扱われます。 
この値は MappingProxyType() でラップされ、読み出し専用になり、 Field オブジェクトに公開されます。 
これはデータクラスから使われることはなく、サードパーティーの拡張機構として提供されます。 
複数のサードパーティーが各々のキーを持て、メタデータの名前空間として使えます。

C#でいうところのAttributeと似たようなものですね。

dataclassでフィールドを定義する際にfield関数を使用してmetadataを設定することができます。
こんな感じ

from dataclasses import dataclass, field

@dataclass
class Company:
    name: str = field(default="", metadata={"metadata": "metadata_value"})
    address: str = field(default="", metadata={"metadata": "metadata_value"})
    average_age: int = field(default=0, metadata={"metadata": "metadata_value"})
    description: str = field(default="")

if __name__ == "__main__":
    company = Company(name="創屋", address="石川県白山市", average_age=30, description="創屋はAIの会社です")

metadataはfields関数で取得したfiledオブジェクトでしか参照できません。

from dataclasses import dataclass, field, fields

@dataclass
class Company:
    name: str = field(default="", metadata={"metadata": "metadata_value"})
    address: str = field(default="", metadata={"metadata": "metadata_value"})
    average_age: int = field(default=0, metadata={"metadata": "metadata_value"})
    description: str = field(default="")

    def show_metadata(self):
        """
        Filed表示
        """
        for field_ in fields(self):
            print(field_.name, field_.metadata)

if __name__ == "__main__":
    company = Company(name="創屋", address="石川県白山市", average_age=30, description="創屋はAIの会社です")
    company.show_metadata()

実行結果

name {'metadata': 'metadata_value'}
address {'metadata': 'metadata_value'}
average_age {'metadata': 'metadata_value'}
description {}

metadataの活用方法

バリデーション

from dataclasses import dataclass, field, fields

@dataclass
class Company:
    name: str = field(default="", metadata={"validation": [lambda x: len(x) == 0]})
    address: str = field(default="", metadata={"validation": [lambda x: len(x) == 0]})
    average_age: int = field(default=0, metadata={"validation": [lambda x: not x > 0]})
    description: str = field(default="")

    def validation(self):
        """
        validation
        """
        for field_ in fields(self):
            validations = field_.metadata.get("validation", [])
            for validation in validations:
                assert not validation(getattr(self, field_.name))

フォーマット設定

from dataclasses import dataclass, field, fields
from datetime import datetime

@dataclass
class Company:
    name: str = field(default="")
    foundation_date: datetime = field(default=datetime.now(), metadata={"format": "%Y%mM%d"})

DBのフィールド名設定

from dataclasses import dataclass, field, fields

@dataclass
class Company:
    name: str = field(default="", metadata={"field_name": "company_name"})
    address: str = field(default="", metadata={"field_name": "company_address"})
    average_age: int = field(default=0, metadata={"field_name": "avg_age"})
    description: str = field(default="", metadata={"field_name": "memo"})

ORMがあれば不要なのですが、ORM使わないケース等では有用かと。

その他

その他には処理対象をフィルタするフラグを付けたり、CSV(エクセル)の書き込み位置を付与したり...
色々と出番はあるかと思います。

最後に

dataclassのmetadataの紹介でした。
Modelの定義はdataclassでできることが多いので、dataclass自体の利用頻度も上がっているのではと思います。
metadataも使いこなせればメンテナンス性の高いスマートなコードになりそうですね。
是非使ってみてください。