Python enumの@uniqueとは?値の重複を防ぐ方法

@uniqueは、Enumの値が重複していないことをクラス定義の時点でチェックするデコレータです。

値をコピペして直し忘れた、といったミスを実行前に止められます。


@uniqueの基本

from enum import Enum, unique

@unique
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(list(Color))

実行結果:

[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

重複がなければ何も起こりません。Enumはそのまま使えます。

インポートはEnumと並べて書きます。

from enum import Enum, unique

重複するとどうなるか

from enum import Enum, unique

@unique
class NoAlias(Enum):
    RED = 1
    CRIMSON = 1

実行結果:

ValueError: duplicate values found in <enum 'NoAlias'>: CRIMSON -> RED

CRIMSON -> REDという形で、どれがどれと衝突したかまで表示されます。

重要なのは、これがクラスを定義した瞬間に出ることです。そのメンバーを使う行ではありません。

つまりインポートした時点で落ちるため、ミスがそのまま本番まで流れません。


@uniqueがないと何が起きるか

from enum import Enum

class Status(Enum):
    PENDING = 1
    RUNNING = 2
    DONE = 2          # 3のつもりが2

print("一覧:", list(Status))
print("len :", len(Status))
print("DONE is RUNNING:", Status.DONE is Status.RUNNING)

実行結果:

一覧: [<Status.PENDING: 1>, <Status.RUNNING: 2>]
len : 2
DONE is RUNNING: True

エラーは出ません。Pythonは値が同じメンバーをエイリアス(別名)として扱うためです。

結果として、

  • Status.DONEはStatus.RUNNINGそのものになる
  • 一覧にDONEが出ない
  • len()が3ではなく2になる

「完了にしたのに実行中と判定される」という気づきにくいバグになります。@uniqueは、これを定義時のエラーに変えてくれます。


エイリアスは悪ではない

値の重複が常にミスとは限りません。意図的な別名として使うこともあります。

from enum import Enum

class Color(Enum):
    RED = 1
    CRIMSON = 1     # 旧名との互換用
    GREEN = 2

print(Color.CRIMSON)
print(list(Color))
print(list(Color.__members__))

実行結果:

Color.RED
[<Color.RED: 1>, <Color.GREEN: 2>]
['RED', 'CRIMSON', 'GREEN']

古い名前を残したまま新しい名前に移行する、といった場面で役立ちます。

使い分けはシンプルです。

状況 @unique
すべて別々の値にしたい 付ける
別名を意図的に用意したい 付けない

迷ったら付けておくのが安全です。あとから別名が必要になったときに外せば済みます。

エイリアスの詳しい挙動は「Python Enumのメンバーを取得する方法|name・value・反復」で解説しています。


型が違っても重複になる

from enum import Enum, unique

@unique
class T(Enum):
    A = 1
    B = 1.0
    C = True

実行結果:

ValueError: duplicate values found in <enum 'T'>: B -> A, C -> A

Pythonでは1 == 1.0 == Trueが成り立つため、型が違っても同じ値とみなされます。

@uniqueを付けない場合は、3つとも1つのメンバーにまとまります。

class U(Enum):
    A = 1
    B = 1.0
    C = True

print(list(U))
print(U.B is U.A, U.C is U.A)

実行結果:

[<U.A: 1>]
True True

一方、文字列の大文字と小文字は別扱いです。

class V(Enum):
    A = 'x'
    B = 'X'

print(list(V))

実行結果:

[<V.A: 'x'>, <V.B: 'X'>]

auto()と組み合わせる

from enum import Enum, unique, auto

@unique
class Status(Enum):
    PENDING = auto()
    RUNNING = auto()
    DONE = auto()

print([(m.name, m.value) for m in Status])

実行結果:

[('PENDING', 1), ('RUNNING', 2), ('DONE', 3)]

auto()は値を自動で振るので、そもそも重複しません。

それでも@uniqueを付けておくと、あとから手書きの値を混ぜたときの保険になります。

@unique
class Mixed(Enum):
    A = 1
    B = auto()      # 2になる
    C = 2           # Bと衝突

実行結果:

ValueError: duplicate values found in <enum 'Mixed'>: C -> B

auto()の値の決まり方は「Python enumのauto()とは?値を自動で割り当てる方法」で解説しています。


IntEnumやFlagにも使える

from enum import IntEnum, unique

@unique
class Level(IntEnum):
    LOW = 1
    MIDDLE = 2
    HIGH = 3

print(Level.HIGH > Level.LOW)

実行結果:

True

@uniqueはEnumを継承したクラスならどれでも使えます。IntEnumやFlagも対象です。

IntEnumについては「Python IntEnumとは?数値として使えるEnumを解説」を参照してください。


@uniqueのまとめ

  • from enum import Enum, uniqueでインポート
  • クラスに付けるだけで値の重複を定義時に検出
  • エラーはValueError: duplicate values found ...
  • 付けないと重複はエイリアスとして黙って通る
  • 1・1.0・Trueは同じ値とみなされる
  • 別名を意図的に使うときだけ外す

あわせて読みたい

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら