Pythonにおいて、Enum
は列挙型クラスをサポートしています。列挙型とは「定義された値の有限集合を表すデータ型」です。
これだけ読むと何のこっちゃという感じですが、たとえば血液型の「A型・B型・O型・AB型」のように、あるいはトランプのスートのようなもの、と考えればいいかもしれません。「あるカテゴリに属し、限られた選択肢の中から1つを取る」値たちのことを集合として扱えるデータ型、とという感じでいいでしょうか。血液型について言えば、1人の血液型は前述の4つ以外に存在しませんし、トランプで「スペードでありハートでもある」なんて状況はないはずです。
プログラムではこれらを「順序を持たない識別子」として利用したいのですが、「スペード」や「AB型」のように文字列として持つとプログラム上の取り回しが非常に悪いです。そのため、識別子を一括管理できる列挙型で扱おうというわけです。
なお、列挙型を実装する際は識別子に任意の整数値を割り当てることが多いです。これにより、コード上は文字列っぽい識別子として振舞っても、実際はその中身である整数値によって管理されている、という感じになります。このように実装することで、コードを管理しやすくなります。
まとめると、列挙型を利用することで以下のようなメリットがあります。
Enum
をインポートし継承することで、オリジナルの列挙型を定義できます。
>>> from enum import Enum
>>> class BloodType(Enum):
... O = 1
... A = 2
... B = 3
... AB = 4
...
>>> BloodType
<enum 'BloodType'>
>>> type(BloodType)
<class 'enum.EnumMeta'>
血液型の列挙型のクラスを作成してみました。メンバーには列挙したい値を設定し、メンバーに対応する値に何らかのオブジェクトを代入します。クラスBloodType
には上記の4つ以外に値が存在しないため、この集合は有限であると言えます。また、O
やAB
というメンバーは「血液型」というカテゴリに属する識別子である、ということを表現できます。これにより、ある集団とそれに属する要素を表現できるわけです。
前述のように列挙型では整数値を入力することのほうがおおいので、上記の例では数値を入力しています。ただ、上記のように連番で振る場合は、手書きでなくauto()
関数を使う場合がほとんどだと思います。
>>> class BloodType(Enum):
... O = auto()
... A = auto()
... B = auto()
... AB = auto()
...
上記のようにauto()
関数を利用することで、手書きする必要なく連番を付与できます。
>>> class BloodType(Enum):
... O = 'O型'
... A = 'A型'
... B = 'B型'
... AB = 'AB型'
...
上記のように文字列を付与することも可能です。
列挙型の対象になるような集合において、値に何らかの意味を持たせる必要がない場合はauto()
で連番を付与し、意味を持たせるなら文字列なり何らかのオブジェクトを設定することになるでしょう。
なお、Enum
は一度クラスを宣言すると、あとでデータを追加することができません。
列挙型はそれ自体をイテレータとして利用できます。
>>> for type in BloodType:
... print(type)
...
BloodType.O
BloodType.A
BloodType.B
BloodType.AB
>>> for type in BloodType:
... print(type.value)
...
O型
A型
B型
AB型
上記の例では、イテレータとしてfor
文で利用しています。なお、ループ処理する際の順序は、そのクラスのメンバーを宣言した順です。
メンバーを参照するには、メンバー名を直接指定するか、設定されている値を指定してメンバーを参照できます。
>>> class BloodType(Enum):
... O = auto()
... A = auto()
... B = auto()
... AB = auto()
...
>>>
>>> BloodType.O
<BloodType.O: 1>
>>> BloodType.B
<BloodType.B: 3>
ただし、メンバー名を直接指定した場合はEnum
クラスが返ってくるので、単純にメンバー名だけ欲しいという場合はちょっと困ってしまいます。
>>> BloodType.O.name
'O'
そこで、.name
を付与することでメンバー名だけを抽出できます。
しかし、メンバーの情報が欲しいのにメンバー名を指定して出力するなんてケースはあまりないと思うので、設定されている値からメンバーを参照する方が利用するケースは多いかもしれません。
>>> BloodType(1)
<BloodType.O: 1>
>>> BloodType(3)
<BloodType.B: 3>
BloodType
クラスには連番が振られているため、その数値を指定することでメンバーを参照できます。
>>> BloodType('A型')
<BloodType.A: 'A型'>
この方法は、メンバーに対応する値が文字列でも数値と同様に参照可能です。
>>> BloodType('くわがた')
ValueError: 'くわがた' is not a valid BloodType
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/enum.py", line 304, in __call__
return cls.__new__(cls, value)
File "/usr/lib/python3.8/enum.py", line 595, in __new__
raise exc
File "/usr/lib/python3.8/enum.py", line 579, in __new__
result = cls._missing_(value)
File "/usr/lib/python3.8/enum.py", line 608, in _missing_
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 'くわがた' is not a valid BloodType
該当するような値が存在しない場合、エラーを吐きます。
列挙型で宣言した値の参照は、ちょっとコツが要ります。
>>> BloodType.O
<BloodType.O: 1>
>>> BloodType.B
<BloodType.B: 3>
上記のようにメンバーのみを記述して参照しようとすると、Enum
クラスとして結果が返ってきますが、対応する値そのものは返ってきません。
>>> print(BloodType.O)
BloodType.O
ちなみに、print()
で出力しようとするとクラス名とメンバ名だけが表示され、値の出力がなくなります。なんでや、今欲しいのは値の方なんだってば。
>>> BloodType.O.value
1
じゃあ、値を参照したい場合はどうするかと言うと、上記のように.value
を付与します。
>>> print('matched') if BloodType.O == 1 else print('unmatched')
unmatched
>>> print('matched') if BloodType.O.value == 1 else print('unmatched')
matched
前述のとおり、BloodType.O
とだけ記述するとEnum
クラスとして扱われるため、if
などにより値の比較をしたい場合は.value
を付与する必要があります。Enum
クラスとint
で直接比較できないので、まぁ当然と言えば当然ですね。整数で直接比較したいなら、Enum
ではなくintEnum
クラスから継承しましょう。
>>> BloodType.A
<BloodType.A: 'A型'>
>>> BloodType.A == 'A型'
False
なお、この挙動は値が文字列でも同様です。
>>> BloodType.O == BloodType.O
True
>>> BloodType.O == BloodType.A
False
Enum
クラス同士であれば比較は可能です。
Literal
型との違い有限の集合を表現する方法はEnum
だけでなく、typing.Literal
でも実現可能です。
>>> from typing import Literal
>>> def hoge(value: Literal['foo', 'bar', 'baz']) -> None:
... print(value)
>>> def fuga(value: Literal[1, 2, 3]) -> None:
... print(value*2)
上記のようにtyping
からLiteral
をインポートして利用します。
Literal
を使用した場合、限定したい要素を数字や文字列などで直接指定します。一方、Enum
を使用した場合は、Enum
クラスを継承しメンバーと、そのメンバーに対する値をそれぞれ設定します。
限定したいデータが「1つのカテゴリに属するもの」でなく、単純にデータの集合としてのみ表現したいなら、Literal
を利用した方がコードの可読性向上を期待できそうです。また、「特定の箇所でのみ利用し使いまわさない」というような場合は、ラムダ式による無名関数を利用するようにLiteral
を利用した方がムダがなくて見通しの良いコードになるでしょう。
反面、Literal
では、文字列なり数値なり何らかのオブジェクトをただただ列挙することしかできません。何かに属するメンバーとしてまとめて管理したい、名前だけでなくそれに対応する値も保持したい、コード全体で使いまわす必要がある・・・というような利用シチュエーションではEnum
に分があるでしょう。
>>> from enum import Enum
>>> class AlartReasons(Enum):
... NOTNUMBER = '数字じゃない'
... NOTSTRING = '文字列じゃない'
... ZEROLENGTH = '1文字も入力されていない'
...
>>> AlartReasons
<enum 'AlartReasons'>
>>> AlartReasons.NOTNUMBER
<AlartReasons.NOTNUMBER: '数字じゃない'>
>>> print(AlartReasons.NOTNUMBER)
AlartReasons.NOTNUMBER
>>> print(AlartReasons.NOTNUMBER.value)
数字じゃない
>>> print(AlartReasons.NOTNUMBER.name)
NOTNUMBER
上記のように「何らかの名前を付けた上でオブジェクトを宣言したい」など、名前と値をセットで利用したい場合はEnum
の方がいいかもしれません。ただ、上記のような場合はそもそもEnum
じゃなくて辞書型でも使った方がいいような気がしますし、もう少し複雑になるようであれば独自のクラスを作成した方が読みやすいでしょう。