Pythonのdataclassにおけるmetadata

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

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

こんな感じ
```python
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とは
以下[公式](https://docs.python.org/ja/3.10/library/dataclasses.html)より参照
```
metadata: これはマッピングあるいは None に設定できます。 None は空の辞書として扱われます。 
この値は MappingProxyType() でラップされ、読み出し専用になり、 Field オブジェクトに公開されます。 
これはデータクラスから使われることはなく、サードパーティーの拡張機構として提供されます。 
複数のサードパーティーが各々のキーを持て、メタデータの名前空間として使えます。
```

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

dataclassでフィールドを定義する際にfield関数を使用してmetadataを設定することができます。
こんな感じ
```python
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オブジェクトでしか参照できません。
```python
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()
```

実行結果
```bash
name {'metadata': 'metadata_value'}
address {'metadata': 'metadata_value'}
average_age {'metadata': 'metadata_value'}
description {}
```

# metadataの活用方法
## バリデーション
```python
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))
```

## フォーマット設定

```python
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のフィールド名設定
```python
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も使いこなせればメンテナンス性の高いスマートなコードになりそうですね。
是非使ってみてください。