第127回 実務で使えるPython基礎:型ヒントとdataclassesで作る読みやすくメンテしやすいAIワークフローモデル

はじめに — つまずきに寄り添って

AIワークフローを実装していると、「このJSON、どこまで信頼していいのか」「設定ファイルが散らばっていて変更が怖い」といった不安に直面することが多いはずです。動くコードを書けても、数カ月後に自分やチームが読み直したときに理解できるかは別問題です。本記事では、Pythonの型ヒントとdataclassesを用いて、APIペイロードやジョブ設定などのデータ構造を明示化し、可読性と保守性を高める実務的手順を段階的に示します。

この記事の狙いと対象

対象は、AIを業務に組み込みたい実務担当者、個人事業主、中小企業の担当者。実運用でよく使う入力/出力スキーマ、設定、バッチ処理のメタ情報を、安全に扱う具体的な方法を学べます。必要な手順はコード中心に示しますが、解説は現場で使える実務目線を重視します。

実務でのユースケース(概要)

用途 扱うデータ 型ヒント / dataclass の利点
モデル入力/出力のスキーマ定義 プロンプト/パラメータ、レスポンスJSON 構造が明確に、IDE補完や静的チェックが効く
API連携で受け取るJSONのマッピング リクエスト/レスポンスのペイロード マッピングコードがシンプルになり、誤変換を減らせる
ジョブ設定/スケジュールパラメータ バッチ設定、リトライ回数など スキーマ変更時の影響範囲が見えやすい

ステップ別ハンズオン(段階的に導入)

1) 関数に型注釈を付ける(最小限の導入)

まずは既存の関数に戻り値と引数の型を付けるだけ。静的解析ツール(mypy)やIDEの補完が効くようになります。

def call_model(prompt: str, max_tokens: int = 256) -> dict:
    # ここでAPI呼び出し
    return {"text": "..."}

2) 基本的な@dataclass定義

入力/設定を示す小さなdataclassを作ります。可読性が上がり、初期化時の意図が明確になります。

from dataclasses import dataclass

@dataclass
class InferenceRequest:
    prompt: str
    max_tokens: int = 256
    temperature: float = 0.0

req = InferenceRequest(prompt="こんにちは")

3) ネストやOptional、List対応

実務ではネスト構造や任意フィールドが必須になります。typingを組み合わせて表現します。

from typing import List, Optional
from dataclasses import dataclass

@dataclass
class Metadata:
    job_id: str
    retries: int = 0

@dataclass
class BatchItem:
    id: str
    input_text: str
    metadata: Optional[Metadata] = None

@dataclass
class BatchRequest:
    items: List[BatchItem]

4) dict/JSONとの相互変換パターン

APIや外部ファイルとの入出力にはシリアライズ/デシリアライズが必要です。シンプルなfrom_dict/to_dictパターンを示します。

def batch_item_from_dict(d: dict) -> BatchItem:
    meta = d.get("metadata")
    metadata = Metadata(**meta) if meta else None
    return BatchItem(id=d["id"], input_text=d["input_text"], metadata=metadata)

def batch_request_from_json(j: dict) -> BatchRequest:
    items = [batch_item_from_dict(it) for it in j.get("items", [])]
    return BatchRequest(items=items)

注意:ネストが深くなる場合は汎用的な変換ユーティリティ(再帰的なfrom_dict)を用意すると便利です。

実運用での検証手順

__post_init__ を使った簡易バリデーション

dataclassの __post_init__ でランタイムチェックを行うと、早期に異常を検出できます。

from dataclasses import dataclass

@dataclass
class InferenceRequest:
    prompt: str
    max_tokens: int = 256

    def __post_init__(self):
        if not self.prompt:
            raise ValueError("prompt は空にできません")
        if not (1 <= self.max_tokens <= 2048):
            raise ValueError("max_tokens が範囲外です")

typing.get_type_hints を使った実行時チェック(簡易実装)

静的な型注釈を参照して動的にチェックすることで、受け取ったdictを検証できます。重いバリデーションは別途ライブラリに任せ、軽いチェックは自前で行うとバランスが良いです。

from typing import get_type_hints

def validate_dataclass(dc_cls, data: dict):
    hints = get_type_hints(dc_cls)
    for k, t in hints.items():
        if k not in data:
            continue
        # 型の単純チェック(詳細は省略)
        if not isinstance(data[k], t) and data[k] is not None:
            raise TypeError(f"{k} は {t} 型ではありません")

mypyでの静的チェックとCI組み込み

mypyを導入してコードベースを継続的にチェックします。CIの例:

  • ローカルで mypy --strict を回す
  • GitHub Actionsでpull request毎に mypy と pytest を実行

移行と運用のベストプラクティス

  • 漸進的導入:まずは新しいモジュールでdataclassを採用し、既存コードは段階的に置換する。
  • mutable defaultの落とし穴:リストやdictのデフォルトは field(default_factory=list) を使う。
  • スキーマ変更のバージョニング:breaking changeはマイナー/メジャーでバージョンを付け、後方互換を維持する処理(フォールバック)を用意する。

周辺ツールとの連携例

現場では複数のライブラリや入力源と連携します。代表的なパターンを示します。

入力源 dataclassとの接続 ポイント
argparse / CLI parse_args を dataclass にマッピング 型変換と必須チェックを集中させると便利
環境変数 os.getenv → 型変換 → dataclass 初期化 欠落時のデフォルトと明示的な変換を用意する
FastAPI / requests 受信したJSONをdataclassに変換して処理、応答はdict化して返却 FastAPIはPydanticを推奨だが、軽量な用途ではdataclassでも十分

pydanticとの使い分け

pydanticは強力なバリデーションと自動変換を提供します。次のように使い分けます。

  • 軽量で依存を増やしたくない:標準のdataclasses + 簡易バリデーション
  • 複雑な変換や詳細なバリデーションが多い:pydanticを採用

テストとデバッグ

dataclassを使った単体テストは簡潔です。ポイントはシリアライズ/デシリアライズ、境界値チェック、例外発生を確認すること。

def test_batch_from_json():
    j = {"items": [{"id": "1", "input_text": "a"}]}
    br = batch_request_from_json(j)
    assert len(br.items) == 1

def test_inference_request_validation():
    try:
        InferenceRequest(prompt="", max_tokens=10)
        assert False, "空のpromptで例外が出るはず"
    except ValueError:
        pass

実践チェックリスト

項目 確認ポイント
型注釈の導入範囲 外部と接するAPI境界、設定ファイル、ジョブ定義に優先的に追加
ランタイム検証 __post_init__ で必須チェックを導入する
CI連携 mypy と pytest をPRごとに実行
デフォルト値 mutable default は default_factory を利用

よくある失敗パターン

  • デフォルトで mutable を使ってしまう(共有状態のバグ)
  • 外部データをそのまま代入して型を信頼しすぎる(早めにバリデーションを入れる)
  • 全コードを一気に型化しようとして途中で挫折する(漸進的に進める)

次の一歩(短い演習)

記事付属のサンプルリポジトリ(記事末リンク想定)を使って、次の小さな演習を試してください:

  • CSVを読み込み、各行をdataclassにマッピングしてバッチ推論を行うスクリプトを作る
  • mypy と pytest を設定してCIで走らせる
  • 簡易的な __post_init__ バリデーションを追加して不正データを早期に検出する

まとめ

型ヒントとdataclassesは、AIワークフローで扱うデータ構造を明示化し、可読性・保守性を高める有力な手段です。まずは関数への型注釈と小さなdataclassから始め、シリアライズ/デシリアライズ、簡易バリデーション、CIでの静的チェックを順に導入することで、現場で使える堅牢な基盤を築けます。Pydanticのようなツールも選択肢に入れつつ、現場のコストと求めるバリデーションレベルに応じて使い分けてください。

Manage AI では、今回のような実務に直結する小さな改善を積み重ねることを推奨しています。まずは手元の一つのスクリプトにdataclassを導入して、効果を確かめてみましょう。