dataclass の“罠”を回避し、実践で使えるパターンを大量紹介 ―
@dataclass を使っていると、必ず登場するのが default_factory。
「なんとなく list の初期値をつけるために使う」と理解している方も多いですが、実はもっと奥が深く、高度な初期化ロジック・安全なミュータブル管理・ID生成・依存性注入 まで幅広く応用できます。
この記事では、default_factory を 本質から理解し、実務で使いこなすための実例をどこよりも丁寧に 紹介します。
❖ 目次
-
default_factoryとは? - なぜ
default=ではダメなのか - 基本例(list / dict / set)
- 関数を使った初期化
- ラムダを使った柔軟な初期化
- クラス生成を使うパターン
- 日付や UUID を使うパターン
- 依存性注入(DI)の初期化
- 環境変数・設定読み込みと併用する
- キャッシュ・シングルトンとの組み合わせ
- まとめ
1. default_factory とは?
field(default_factory=...) は 「インスタンスを作るたびに呼ばれる関数」 を指定する仕組みです。
field(default_factory=list)
なら「インスタンス生成時に list() を実行して初期値を作る」という意味。
2. なぜ default= ではダメなのか?
可変オブジェクトを初期値にすると、インスタンス間で共有される という致命的な罠があります。
@dataclass
class Bad:
values: list[int] = [] # ❌ 危険
a = Bad()
b = Bad()
a.values.append(1)
print(b.values) # → [1](共有されてしまう!)
✓ default_factory を使うべき理由
@dataclass
class Good:
values: list[int] = field(default_factory=list)
a = Good()
b = Good()
a.values.append(1)
print(b.values) # → [](完全に分離)
default_factory は 新品のオブジェクトを毎回作る 工場(factory)の役割を持ちます。
3. 基本例(list / dict / set)
list
items: list[int] = field(default_factory=list)
dict
config: dict[str, str] = field(default_factory=dict)
set
tags: set[str] = field(default_factory=set)
これが最もよく見る基本パターンです。
4. 関数を使った初期化(中級)
処理をカスタマイズしたいときは関数を渡せます。
def initial_permissions():
return {"read": True, "write": False}
@dataclass
class User:
permissions: dict = field(default_factory=initial_permissions)
5. ラムダを使った柔軟な初期化
関数を書くほどでもない場合は lambda が便利。
@dataclass
class Config:
ports: list[int] = field(default_factory=lambda: [80, 443])
6. クラス生成を使うパターン(オブジェクト初期化)
default_factory は「関数」なので、もちろんクラス呼び出しも使えます。
class Logger:
def __init__(self):
self.buffer = []
@dataclass
class Service:
logger: Logger = field(default_factory=Logger)
インスタンスごとに独立した Logger が生成されます。
7. 日付や UUID を使うパターン(実務でよく使う)
UUID
import uuid
@dataclass
class Record:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
タイムスタンプ
from datetime import datetime
@dataclass
class Job:
created_at: datetime = field(default_factory=datetime.utcnow)
8. 依存性注入(DI)に使う(上級)
default_factory は軽量 DI コンテナとして使えます。
class ApiClient:
...
def create_client():
return ApiClient(base_url="https://api.example.com")
@dataclass
class Repository:
client: ApiClient = field(default_factory=create_client)
テスト時だけ client を差し替えることも簡単。
9. 環境変数・設定読み込みと組み合わせる
import os
def load_mode():
return os.getenv("APP_MODE", "dev")
@dataclass
class Config:
mode: str = field(default_factory=load_mode)
「起動時に自動で環境変数を読む」仕組みを簡単に導入できます。
10. キャッシュ・シングルトンとの併用
グローバルシングルトンを返す factory も作れます。
class Cache:
...
cache_instance = Cache()
@dataclass
class Service:
cache: Cache = field(default_factory=lambda: cache_instance)
全インスタンスで同じキャッシュを共有したい場面に便利。
✔ 応用:読み込みの重い設定をキャッシュする
config_cache = None
def load_config_once():
global config_cache
if config_cache is None:
print("Loading config...")
config_cache = {"mode": "prod"}
return config_cache
@dataclass
class App:
config: dict = field(default_factory=load_config_once)
11. まとめ
| やりたいこと | 書き方 | メリット |
|---|---|---|
| ミュータブルな初期値 | default_factory=list |
共有バグを防ぐ |
| デフォルト設定を作りたい | 関数 or lambda を使う | 柔軟な初期化 |
| ランダム値・UUID を生成 | default_factory=lambda: ... |
実務で超便利 |
| オブジェクト注入 | factory にクラスを渡す | DIとして使える |
| 設定の遅延読み込み | factory 内でキャッシュ管理 | 高パフォーマンス |
🚀 結論
default_factory は単なる「list の初期値」ではありません。
安全性、可読性、柔軟性を強化し、dataclass を“本物の設計ツール”へ格上げしてくれる存在 です。
実務では:
- 設定管理
- Web アプリ
- API クライアント
- バッチ処理
- DI コンテナ
- 構成オブジェクト生成
など、ほぼすべての層で役立ちます。