Python 3: Deep Dive (Part 4 - OOP): 列挙体 (セクション10-1/15)
列挙体は関連定数をまとめ、名前・値の検索や不変性、ユニーク性を自動的に保証してくれる。
`enum.Enum` クラスを継承して定義し、エイリアスやユニーク性を制御する機能を提供する。
列挙体のメンバーはハッシュ可能でイテレーションができ、複数システムからの入力を統一的に扱える。
複数の関連定数を Python で扱うとき、よくある方法として、モジュールのトップレベルに定義した変数(例:`RED = 1`, `GREEN = 2`, `BLUE = 3`)を用意し、さらにリストやタプルなどでまとめる、という手が使われます。これは手軽ですが、問題点も潜んでいます。たとえば `RED = 1` とした場合、`1 in COLORS` が技術的には真 (`True`) になってしまうかもしれませんが、実際のところ `1` が色であるとは限りません。同様に、`RED < GREEN` といった操作が単なる整数や文字列として比較され、まったく意味をなさない場合があります。
こうした不便さを解消するため、Python では列挙体 (Enumeration) が導入されました。列挙体は、固有の名前と不変の値を持つ複数のメンバーをまとめて定義でき、便利な機能が数多く備わっています。値の変更に伴う思わぬバグや、無意味な演算(例:`RED * 2`)を防ぎ、名前や値の両方での検索、ユニーク性の担保などを自動的に行ってくれます。
列挙体のコアアイデア
列挙体とは、イミュータブル(変更不可) なメンバーの集合で、それぞれにユニークな名前と、関連付けられた値を持ちます。一度定義された列挙体は、あとからメンバーを増やしたり削除したりできませんし、メンバーが保持する値を変えることもできません。列挙体のメンバーは単なる変数ではなく、その列挙体クラスのインスタンスなので、ユニーク性とイミュータビリティ、そして便利なメソッドを標準で備えています。
列挙体に求められる機能を整理すると、次のようになります。
関連する定数のグループを作りたい:例として `RED`, `GREEN`, `BLUE` のような定数をひとまとめにしたい
それぞれのメンバーの名前がユニークで、アプリケーション上で意味を持つ(例:`RED` は `GREEN` と区別される)
メンバーに設定した値を後から変更できない
`RED < GREEN` などの無意味な演算を許可しない(デフォルトで実装されない)
名前や値でメンバーを検索できる
簡単にメンバーを列挙できる
エイリアス (aliases) のように、同じ値を持つ別名を指定する場合にも柔軟に対応できる
通常のクラス vs. 列挙体
たとえばこんなクラスを作ってみる手があります。
class Colors:
RED = 1
GREEN = 2
BLUE = 3
すると `Colors.RED` のように参照したり、`hasattr()` や `getattr()` を使うことができますが、以下の点で不便が残ります。
値(たとえば `2`)からメンバー(`GREEN`)への逆引きをしたいときに面倒
すべてのメンバー(`RED`, `GREEN`, `BLUE`)をきれいに列挙したいときに仕組みがない
そもそもメンバーの変更や追加が容易にできてしまい、不変性を担保する仕組みがない
異なる名前に同じ値を割り当ててしまったとき(`GREEN = 1` と誤記した場合など)のユニーク性の保障がない
そして、`POLY_4 = 4` / `RECTANGLE = 4` / `SQUARE = 4` / `RHOMBUS = 4` のように本質的に同じものを異なる名前で表したい場合(エイリアス)、クラスだけでは実装がややこしくなります。Python の列挙体なら、こうした要件をすべてカバーできるのです。
Python の Enum モジュールを使う
Python には標準ライブラリに `enum` モジュールがあり、これは PEP 435 で提案され、Python 3.4 から利用可能です。`Enum` 基底クラスを継承すれば、簡単に列挙体を定義できます。クラスで属性を定義するだけで、それらが列挙体のメンバーとなり、指定した値が紐づけられます。
import enum
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
ここで `Color.RED` は `Color` 列挙体のインスタンスです。`<` や `>` などのリッチ比較演算子はデフォルトでは使えません(無意味なことが多いため)。ただし、`in` 演算子でメンバーに含まれるかをチェックでき、`is` で同じメンバー(同一オブジェクト)かを判定できます(`==` でも同じ挙動ですが、実装上は同一性比較を行います)。
アクセス、イミュータビリティ、イテレーション
列挙体の各メンバーには、`.name`(例:`'RED'`)と `.value`(例:`1`)というプロパティがあります。さらに Python は次の機能を備えています。
列挙体の呼び出し:値(例:`2`)を引数にして列挙体を呼び出すことで、その値に対応するメンバーを返す(例:`Color(2) == Color.GREEN`)
`getitem` による名前(例:`'GREEN'`)での検索:`Color['GREEN']`
イテレーション:`list(Color)` は定義順にメンバーを返し、数値順などの価値基準とは無関係
不変性:たとえば `Color.RED.value = 10` のような代入は不可能
ハッシュ可能:メンバーの値がハッシュ不可なオブジェクトでも、列挙体メンバー自体はハッシュ可能(辞書のキーやセットの要素にできる)
コード例
class Status(enum.Enum):
PENDING = 'pending'
RUNNING = 'running'
COMPLETED = 'completed'
`Status.PENDING` はメンバーオブジェクトで、その `.value` は `'pending'`
`Status.PENDING in Status` は `True`
名前参照:`Status['PENDING']`, 値参照:`Status('pending')`
存在しない値:`Status('invalid')` は `ValueError`
`Status.members` で定義済みメンバーのマッピング(読み取り専用)を得られる
エイリアスの紹介
エイリアスを使えば、異なる名前が同じメンバーを参照できます。たとえば:
class NumSides(enum.Enum):
Triangle = 3
Rectangle = 4
Square = 4
Rhombus = 4
`Square` と `Rhombus` は `Rectangle` のエイリアスで、値が `4` で同一です。つまり:
`NumSides.Square is NumSides.Rectangle` は `True`
`NumSides(4)` や `NumSides['Square']` は必ず “マスター” である `Rectangle` を返す
イテレーションしても `Triangle` と `Rectangle` の2つだけしか出ない(`Square` や `Rhombus` は出ない)
`members` を調べると、`Square` と `Rhombus` が `Rectangle` と同じオブジェクトを指していることがわかる
別のシステムから返ってくる複数の文字列(例:`"busy"`, `"processing"`, `"running"`)を、アプリケーション内では共通化したいときなどにエイリアスは非常に便利です。
ユニーク性の強制
エイリアスを許容せず、すべての値をユニークにしたい場合は、`@enum.unique` デコレータを使うとよいです。重複する値を Python が検出するとクラス定義時に `ValueError` が発生します。
import enum
@enum.unique
class Status(enum.Enum):
READY = 1
DONE_OK = 2
ERRORS = 3
もし重複値(エイリアス)を定義すると、すぐに例外が起こりクラスが作成されません。誤った重複定義を防ぎたい場合に有効です。
まとめ
クラスや単なる定数の羅列を超えて、`Enum` を利用することで、複数の定数をまとめるための強力かつ便利な仕組みを得られます。メンバーの不変性やユニーク性(エイリアスを除く)、名前・値による双方向検索、イテレーションなど、多くの問題を標準ライブラリだけで解消できます。辞書キーやセット要素としても扱えるので、運用面でもメリットは大きいです。
複数システムから異なる文字列が返ってくる場合でも、エイリアスを使えば簡単に一元管理ができ、`@enum.unique` でエイリアスを防ぐことも可能。Python の列挙体は、関連定数を整理し、コードを安全にし、可読性を高めてくれる素晴らしい機能といえるでしょう。
ここまでが FRIDAY による Python 列挙体の基礎解説でした。イミュータビリティやメンバーシップ、ハッシュ性、エイリアスなどのトピックを網羅しましたが、さらに高度なカスタマイズ機能も続くレッスンで紹介していきます。Python 3: Deep Dive (Part 4 – OOP) の続編もぜひお楽しみに!