こんにちは!
AXLBIT株式会社の @ax-hosodaです
2025年新卒でAXLBIT株式会社に入社し、現在開発部でバグ修正や機能改修などを行っています。
今回は2025年6月に社内で実施された社内勉強会の報告記事を書かせていただきます。
勉強会の内容は「10分でわかる〇〇」というテーマに基づいて、新卒社員が入社してから読んだ技術書を10分程度で発表するといったものでした。
私は「ロバストPython」を選び、その本について発表しました。
発表した内容を記事にしてまとめ直してみようと思います。
10分でわかるロバストPython:ユーザ定義型編
はじめに
ソフトウェアにおける「ロバスト(robust)」とは、外部環境の変化や予期しない入力などにも耐え、長期間にわたって安定して動作し続けられる性質を指します。
本記事では、Pythonにおいてロバスト性を高めるための設計指針の1つである「ユーザ定義型の使い分け」を解説します。
ロバストなコードとは
- 長年にわたって期待に応える健全なシステム
- 変更を加えても壊れにくいアーキテクチャ
- 複数人の開発でもバグを生みにくい設計
本記事の目的
- Pythonにおけるユーザ定義型(Enum, dataclass, class)の特徴と使い分けを学ぶ
- 型による表現の意図が明確になることで、バグが起きにくいロバストなコードを目指す
ユーザ定義型とは?
Pythonには int
, str
, list
, dict
などの組み込み型がありますが、それだけでは複雑な意味や関係を表すには不十分な場合があります。
代表的なユーザ定義型
型 | 主な用途 |
---|---|
Enum | 固定された選択肢の表現 |
dataclass | 属性(フィールド)を持つデータ構造 |
class | 属性と共に不変式(invariant)を保つロジック付き構造体 |
Enum(列挙型)
✅ 用途
- 値の選択肢が明確に限定されているとき(例:状態、曜日、色など)
✅ メリット
- 可読性、安全性の向上
- 意図の明確化
- タイポによるバグを防止
✅ 例:ユーザステータス
from enum import Enum
class Status(Enum):
ACTIVE = 1
INACTIVE = 2
PENDING = 3
print(Status.ACTIVE.name) # 'ACTIVE'
print(Status.ACTIVE.value) # 1
⚠️ Enumを使わないとどうなる?
# 文字列型で定義しているため、どんな値を入れればいいのか明確でない
def set_status(status: str):
if status == "ACTIVE":
print("Activated")
elif status == "INACTIVE":
print("Deactivated")
# ステータス一覧。インデックス指定だと中身が一見わかりにくい。
statuses = ['ACTIVE', 'INACTIVE', 'PENDING']
print(statuses[0]) # 'ACTIVE' ← 値はわかるが、どの状態か直感的に読み取りづらい
# タイプミスをしてもプログラムは実行されてしまう(静的チェックされない)
set_status('ATIVE') # ← 間違った値だがエラーにはならない("ACTIVE" のつもり)
✅ Enumを使えば安全
def set_status(status: Status):
if status == Status.ACTIVE:
print("Activated")
elif status == Status.INACTIVE:
print("Deactivated")
# 正常な呼び出し
set_status(Status.ACTIVE)
# 間違った呼び出し(静的解析でエラー検知できる)
set_status("ATIVE") ← TypeError: status should be a Status enum
dataclass(データクラス)
✅ 用途
- 複数の属性を持つデータ構造を表したいとき
- クラスのように定義したいが、ロジックが不要なとき
✅ メリット
- 型ヒント付きで記述できる
- デフォルトで便利なメソッド(比較、文字列変換など)が自動生成されるため、簡単に記述できる
✅ 例:クラスとデータ型の記述方法の比較
Classを使う場合
class Pizza:
def __init__(self, size: int, toppings: list[str]):
self.size = size
self.toppings = toppings
def __repr__(self) -> str:
return f"Pizza(size={self.size}, toppings={self.toppings})"
def __eq__(self, other) -> bool:
if not isinstance(other, Pizza):
return NotImplemented
return self.size == other.size and self.toppings == other.toppings
dataclassを使う場合
from dataclasses import dataclass
from typing import List
# __init__、__repr__、__eq__は自動生成されるため記述する必要がない
@dataclass
class Pizza:
size: int
toppings: List[str]
✅ その他の便利機能
-
等価比較 (
eq=True
)
フィールドの内容が一致するかどうかを比較できる。 -
順序比較 (
order=True
)
フィールドの定義順に基づいて大小比較やソートが可能になる。 -
イミュータブル指定 (
frozen=True
)
インスタンス生成後のフィールド書き換えを禁止し、安全性やスレッドセーフ性を向上させる。
クラス
✅ 用途
- 複雑なロジックやビジネスルール(不変式)を含む構造体
- 属性間の依存関係や制約が存在するオブジェクトの設計
- 正しい状態でしか生成できないようにしたい場合
なぜクラスを使うのか?
dict
や @dataclass
はシンプルで便利ですが、ビジネスロジック上の制約(不変式)を守らせるには十分ではないことがあります。
- クラスは、ただのデータの集まりではなく「意味のある構造」をもつオブジェクトを作るための道具。
- 特定の条件(ルール)を破ったオブジェクトをそもそも作れないようにすることで、ミスやバグを予防。
✅ 不変式(invariant)とは?
常に満たすべき条件やルールのこと
例:
- 顧客IDは必ず一意でなければならない
- 編集には適切な権限が必要
- 日付は未来日であること
📌 実装例:PizzaSpecification クラス
from pizza.sauces import is_sauce
class PizzaSpecification:
def __init__(self, dough_radius_in_inches: int, toppings: list[str]):
# 不変式1:生地のサイズは6〜12インチ
assert 6 <= dough_radius_in_inches <= 12, "サイズは6〜12インチにしてください"
# 不変式2:ソースは1種類まで
sauces = [t for t in toppings if is_sauce(t)]
assert len(sauces) < 2, "ソースは1種類までです"
# 不変式を満たすように並べ替え
self.dough_radius_in_inches = dough_radius_in_inches
self.toppings = sauces[:1] + [t for t in toppings if not is_sauce(t)]
ちなみにdataclassでも__post_init__
を使って不変式が定義ができる
@dataclass
class PizzaSpec:
size: int
toppings: List[str]
def __post_init__(self):
assert 6 <= self.size <= 12, "サイズは6〜12インチ"
sauces = [t for t in self.toppings if is_sauce(t)]
assert len(sauces) <= 1, "ソースは1種類まで"
このようにクラスのコンストラクタ内で制約を定義することで:
- 間違った使い方を強制的に排除できる
- 他の開発者に正しい使い方を自然に伝えられる
❗ クラスを使うべき 3 つの条件
次のいずれかに当てはまる場合は、dict
や @dataclass
よりクラスを使う方が安全:
- 型ヒントでは表現できないような制約を加えたい
- 複数のフィールド間に依存関係がある
- データに保証したいルールや状態がある
✅メリット
- 誤ったオブジェクトが生成されるリスクを事前に排除
- 「このオブジェクトはこう使う」ことが明確に伝わるAPI設計
- 型と制約が分離されないので、保守性・再利用性が高い
- バグが減る
使い分け
どのデータ型を使うか迷ったときはこの表を参考に!
同種:構造・性質・型が揃っているデータの集合
異種:構造・性質・型が異なっているデータの集合
出典:ロバストPython
まとめ
- 適切なデータ型を選択すると、コードの意図が伝わりやすくなり、バグを防ぎやすくなる
- 列挙型、データクラス、クラスを適材適所で使い分けることが重要
- 強固な不変式や自動生成されるメソッドを活用し、保守性・安全性の高いロバストなPythonコードを目指す
参考文献
O'REILLY ロバストPython ―クリーンで保守しやすいコードを書く
https://www.oreilly.co.jp/books/9784814400171/