Pythonのenumを活用した堅牢な定数管理
概要
Pythonで「ステータス」や「種別」といった固定の選択肢を扱うとき、文字列や整数のリテラルをそのまま書いてしまいがちです。しかしこの方法はタイプミスや値の重複に気づきにくく、コードが育つほど壊れやすくなります。
標準ライブラリの enum モジュールを使うと、取りうる値の集合を型として表現でき、補完・型チェック・比較の安全性がまとめて手に入ります。本記事では入門として、基本から実務で効く使い方までを順を追って整理します。
環境
- Python 3.11 以降を想定(
StrEnumは 3.11 で追加) - 標準ライブラリ
enumのみ使用(追加インストール不要)
3.10 以前でも Enum / IntEnum は利用できます。StrEnum に相当する挙動は class Status(str, Enum) で代替できます。
「生の定数」で起きていた問題
enum を使わない書き方から見てみます。
def update_order(order, status: str):
if status == "shipped":
order.shipped_at = now()
elif status == "canceld": # タイポ。だが実行時まで気づかない
order.canceled_at = now()
この書き方の弱点は次の通りです。
"canceld"のようなタイポをエディタも型チェッカも検出できない- どんな値が「正しい状態」なのか、コードを読んでも一覧が分からない
- 値を一括変更したいとき、文字列を全文検索する羽目になる
モジュール変数での定数化も一歩前進ですが、集合としてのまとまりがありません。
STATUS_PENDING = "pending" STATUS_SHIPPED = "shipped" STATUS_CANCELED = "canceled" # これらが「同じ種類の値」であることはコード上で表現されていない
基本: Enum を定義する
enum.Enum を継承すると、関連する定数を1つの型にまとめられます。
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
SHIPPED = "shipped"
CANCELED = "canceled"
各メンバーは「名前(name)」と「値(value)」を持ちます。
>>> OrderStatus.SHIPPED.name 'SHIPPED' >>> OrderStatus.SHIPPED.value 'shipped'
メンバーは一意で、is による同一性比較ができます。
>>> OrderStatus.SHIPPED is OrderStatus.SHIPPED True >>> OrderStatus.SHIPPED == OrderStatus.PENDING False
型として使う
関数の引数に enum 型を指定すると、「この引数は OrderStatus のいずれか」という意図がシグネチャで表現できます。
def update_order(order, status: OrderStatus) -> None:
if status is OrderStatus.SHIPPED:
order.shipped_at = now()
elif status is OrderStatus.CANCELED:
order.canceled_at = now()
存在しないメンバーを書けばエディタが赤線を出し、mypy などの型チェッカもエラーにします。タイポを実行前に潰せるのが最大の利点です。
値からメンバーへ変換する
外部入力(APIリクエスト、DBの値、CSVなど)は文字列で届きます。EnumClass(value) で値からメンバーへ安全に変換できます。
>>> OrderStatus("shipped")
<OrderStatus.SHIPPED: 'shipped'>
>>> OrderStatus("unknown")
ValueError: 'unknown' is not a valid OrderStatus
不正な値は ValueError になるため、想定外の値がシステム内に入り込むことを境界で止められます。安全なパーサを用意しておくと便利です。
from typing import Optional
def parse_status(raw: str) -> Optional[OrderStatus]:
try:
return OrderStatus(raw)
except ValueError:
return None
IntEnum と StrEnum
数値・文字列そのものとして扱いたい場合は IntEnum / StrEnum が便利です。
from enum import IntEnum, StrEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
class Color(StrEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
これらは int / str のサブクラスなので、比較や連結でそのまま使えます。
>>> Priority.HIGH > Priority.LOW
True
>>> Color.RED == "red"
True
>>> f"color: {Color.RED}"
'color: red'
ただし「何とでも比較できてしまう」ぶん型の厳密さはやや緩みます。JSON シリアライズやDB保存で素の値と混ざる場面では便利ですが、純粋な区分値には素の Enum を使うほうが安全です。
auto() で値の採番を自動化する
値そのものに意味がなく「区別できればよい」場合は auto() が使えます。
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
連番の手書きによる重複ミスを防げます。
実装例: メンバーに振る舞いを持たせる
enum はクラスなので、メンバーに紐づく振る舞いを持たせられます。「値ごとの分岐」を if 文で散らす代わりに、表示名や判定ロジックを enum 自身へ集約できます。
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
SHIPPED = "shipped"
CANCELED = "canceled"
@property
def label(self) -> str:
return {
OrderStatus.PENDING: "処理待ち",
OrderStatus.SHIPPED: "発送済み",
OrderStatus.CANCELED: "キャンセル",
}[self]
@property
def is_terminal(self) -> bool:
"""これ以上状態遷移しない終端状態か"""
return self in (OrderStatus.SHIPPED, OrderStatus.CANCELED)
>>> OrderStatus.SHIPPED.label '発送済み' >>> OrderStatus.PENDING.is_terminal False
全メンバーを列挙する
enum はイテラブルです。選択肢の一覧生成(フォームの選択肢、バリデーションなど)に使えます。
>>> [s.value for s in OrderStatus] ['pending', 'shipped', 'canceled']
値の重複を防ぐ @unique
意図しない値の重複を禁止したい場合は @unique デコレータを付けます。
from enum import Enum, unique
@unique
class OrderStatus(Enum):
PENDING = "pending"
SHIPPED = "shipped"
DONE = "shipped" # 重複 → 定義時に ValueError
定義時(import 時)に ValueError が送出されるため、コピペ由来の重複を早期に検出できます。
確認方法
挙動は対話シェルや簡単なテストですぐ確認できます。
import pytest
def test_value_lookup():
assert OrderStatus("shipped") is OrderStatus.SHIPPED
def test_invalid_value_raises():
with pytest.raises(ValueError):
OrderStatus("unknown")
def test_terminal_states():
assert OrderStatus.CANCELED.is_terminal
assert not OrderStatus.PENDING.is_terminal
注意点
==とis: 素のEnumはメンバーが一意なのでis比較が最も明快です。IntEnum/StrEnumは素の値とも等しくなるため、比較の緩さに注意します。- シリアライズ: 素の
Enumを JSON に出すときは.valueを明示的に渡します。StrEnum/IntEnumなら標準のjson.dumpsでそのまま出せます。 - 値の変更は非互換変更: DBや外部APIに保存される
valueは「公開契約」です。後から変えると既存データとズレるため、nameのリネームとvalueの変更は区別して扱います。 - Enum は書き換え不可: メンバーへの再代入はできません。定数管理という目的に合致しています。
まとめ
- 固定の選択肢は文字列リテラルではなく
enumで型として表現する - 型チェックと補完により、タイポや想定外の値を実行前に潰せる
EnumClass(value)で境界での値変換を安全に行い、不正値はValueErrorで弾く- 数値・文字列として扱いたいときは
IntEnum/StrEnum - メンバーにメソッドやプロパティを持たせ、値ごとの分岐を enum に集約する
@uniqueとauto()で定義ミスを防ぐ
「定数の集合」が出てきたら enum を検討する、という習慣づけだけで、コードの堅牢性は着実に上がります。
