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 を検討する、という習慣づけだけで、コードの堅牢性は着実に上がります。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。