1. Qiita
  2. 投稿
  3. Python

Python 3.4 から標準ライブラリに入る Enum 型が今からでも便利

  • 123
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

今年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