160
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

今年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
160
145
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
160
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?