頑張らないために頑張る

ゆるく頑張ります

PythonにおけるEnumの基本

Posted at — Jul 11, 2023

概要

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つ以外に値が存在しないため、この集合は有限であると言えます。また、OABというメンバーは「血液型」というカテゴリに属する識別子である、ということを表現できます。これにより、ある集団とそれに属する要素を表現できるわけです。

前述のように列挙型では整数値を入力することのほうがおおいので、上記の例では数値を入力しています。ただ、上記のように連番で振る場合は、手書きでなく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じゃなくて辞書型でも使った方がいいような気がしますし、もう少し複雑になるようであれば独自のクラスを作成した方が読みやすいでしょう。

参考

  1. enum — 列挙型のサポート
  2. EnumとFlagはどう違うのか?
  3. そのif文、Enumにしてみませんか。
  4. PythonでEnumをfor文で回したら値をすべて出力しなかった
  5. Pythonで列挙型(Enum)を使いこなす
comments powered by Disqus