今年3月にリリース予定の Python 3.4 から、 enum が追加されます。
(ドキュメント)
Python 2.4 から使えるバックポートが開発されており、 pip install enum34
でインストールできます。
enum が欲しい理由
今までも例えばこんな方法で定数を定義していました。
class Spam:
FOO = 1
BAR = 2
BAZ = 3
しかし、この方法には以下のような問題点があります。
- デバッグ時など、ただの整数値が表示され、意味が分かりにくい
- 値から名前を引く方法を自作しないといけない
- 列挙する方法も自作しないといけない
アドホックに解決したり、ライブラリがあったりしたんですが、これくらい標準ライブラリに欲しいですよね。で、標準ライブラリに入ったんだから、使いましょう。
シンプルな使い方
namedtuple
と同じく、最初に形名、次に名前の一覧のリスト、あるいは空白区切りの文字列を渡すと、勝手に1から順番に値が割り振られます。
できた型は、属性、呼び出し、添字の3種類の方法で、要素にアクセスできます。
>>> import enum
>>> Foo = enum.Enum("Foo", "foo bar baz")
>>> Foo.foo
<Foo.foo: 1>
>>> Foo(1)
<Foo.foo: 1>
>>> Foo["foo"]
<Foo.foo: 1>
>>> isinstance(Foo.foo, Foo)
True
要素には name と value という属性があります。
>>> Foo.foo.value
1
>>> Foo.foo.name
'foo'
要素自体はその値と区別されます。全ての要素はクラス生成と同時に1つだけ作られる singleton なので、 ==
ではなく is
で比較できます。
>>> Foo.foo == 1
False
>>> Foo.foo is Foo(1) is Foo["foo"]
True
>>> Foo.foo is Foo(1)
True
イテレートもできます。
>>> for m in Foo:
... print(m)
...
Foo.foo
Foo.bar
Foo.baz
任意の値を利用する
Enum
を呼び出すのではなく継承したクラスを定義することで、1からの連番ではなく任意の値を要素に割り当てることができます。
>>> class Bar(enum.Enum):
... foo = 'x'
... bar = 'y'
... baz = 'z'
...
>>> Bar.foo
<Bar.foo: 'x'>
Python 2 での制限
Python 2 では、クラスの要素の定義順を利用する方法が(基本的には)ありません。
for 文で列挙するときに定義順を維持したい場合は、 __order__
という属性を定義しておきます。これは Python 2 対応のための、 enum34 による拡張であり、 Python 3.4 の enum モジュールでは何もしなくても定義順が保存されます。
>>> class Bar(enum.Enum):
... __order__ = 'foo bar baz'
... foo = 'x'
... bar = 'y'
... baz = 'z'
...
>>> list(Bar)
[<Bar.foo: 'x'>, <Bar.bar: 'y'>, <Bar.baz: 'z'>]
要素を整数として扱う
要素が直接整数として振る舞うと便利なことありますよね。その場合は IntEnum を使えます。
>>> class Foo(enum.IntEnum):
... foo = 1
... bar = 3
... baz = 5
...
>>> Foo.foo == 1
True
>>> Foo.foo + 2
3
>>> Foo.foo < Foo.bar
True
実は、 IntEnum の定義はたったこれだけです。
class IntEnum(int, Enum):
"""Enum where members are also (and must be) ints"""
このように、要素に持たせたい振る舞いを継承で持たせることができます。
>>> class Bar(str, enum.Enum):
... foo = 'x'
... bar = 'y'
...
>>> Bar.foo + Bar.bar
'xy'
これは便利な半面、動的型付けの Python では、ある変数が持っているのが Enum の要素なのか、純粋な値型なのかわかりにくくなるという危険があります。節度を守って使いましょう。
in について
IntEnum を使っても、整数値と Enum 型の間に直接 in
を使ってその整数値に対応する Enum の値があるかどうかはわかりません。 in
が使えるのはその Enum 型の要素だけです。
>>> class Foo(enum.IntEnum):
... foo = 3
... bar = 7
...
>>> 3 in Foo
False
>>> Foo.foo in Foo
True
EnumMeta を継承してカスタマイズしたり、非公開APIを直接叩けばなんとでもなりますが、そんなお行儀の悪いことはしないで、普通に呼び出して例外でチェックしましょう。
呼び出しで値が見つからなかった時は ValueError
が投げられます。
名前から要素を引くときは、同じように添字記号と KeyError
を使うことができます。
>>> Foo(2)
ValueError: 2 is not a valid Foo
>>> Foo['xyzzy']
KeyError: 'xyzzy'
ただし、公開 API として名前をキーに、要素を値にした辞書が __members__
という名前で用意されているので、名前で引くときは in
とか .get()
とか普段の dict 操作も使えます。
>>> Foo.__members__
{'foo': <Foo.foo: 3>, 'bar': <Foo.bar: 7>}
>>> Foo.__members__['foo']
<Foo.foo: 3>
>>> 'xyzzy' in Foo.__members__
False