Enum HOWTO

Enum HOWTO

프로그램을 짜다 보면 변수가 가질 수 있는 값이 한정된 선택지 안에서만 정해지는 경우가 많아요. 예를 들어 요일이라든가, RGB의 원색이라든가 말이죠. Enum은 이럴 때 쓰는, 고유한 값에 묶인 상징적 이름들의 집합입니다. 전역 변수와 비슷하지만 더 유용한 repr(), 그룹화, 타입 안전성(type-safety) 같은 몇 가지 기능을 더 갖추고 있어요. 이 HOWTO에서는 열거형이 무엇인지, 그리고 파이썬에서 어떻게 활용하는지 실제 코드와 함께 살펴봅니다.

출처: Python 공식 문서 — Enum HOWTO

시작하며

Enum은 제한된 선택지 안에서 값을 고르는 변수가 필요할 때 가장 유용합니다. 요일을 예로 들어 볼게요.

>>> from enum import Enum
>>> class Weekday(Enum):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 3
...     THURSDAY = 4
...     FRIDAY = 5
...     SATURDAY = 6
...     SUNDAY = 7

아니면 RGB 원색처럼요.

>>> from enum import Enum
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3

보시다시피 Enum을 만드는 일은 Enum 자체를 상속하는 클래스를 쓰는 것만큼이나 간단합니다.

참고: Enum 멤버 이름의 대소문자

Enum은 상수를 나타내는 데 쓰이고, mixin 클래스의 메서드·속성과 열거 멤버 이름 사이의 이름 충돌 문제를 피하는 데도 도움이 되므로, 멤버 이름은 UPPER_CASE(대문자)를 강력히 권장합니다. 이 문서의 예시에서도 그 스타일을 따를게요.

열거의 성격에 따라 멤버의 값이 중요할 수도, 아닐 수도 있습니다. 어느 쪽이든 그 값으로 대응하는 멤버를 얻을 수 있어요.

>>> Weekday(3)
<Weekday.WEDNESDAY: 3>

멤버의 repr()은 열거 이름, 멤버 이름, 값을 보여줍니다. str()은 열거 이름과 멤버 이름만 보여줘요.

>>> print(Weekday.THURSDAY)
Weekday.THURSDAY

열거 멤버의 타입은 그것이 속한 열거입니다.

>>> type(Weekday.MONDAY)
<enum 'Weekday'>
>>> isinstance(Weekday.FRIDAY, Weekday)
True

열거 멤버에는 그 name만 담긴 속성이 있습니다.

>>> print(Weekday.TUESDAY.name)
TUESDAY

마찬가지로 value를 위한 속성도 있어요.

>>> Weekday.WEDNESDAY.value
3

많은 언어가 열거형을 단순한 이름/값 쌍으로만 취급하지만, 파이썬의 Enum은 동작(behavior)을 추가할 수 있습니다. 예를 들어 datetime.date에는 요일을 돌려주는 메서드가 두 개 있는데, weekday()isoweekday()입니다. 차이는 하나는 0-6으로 세고 다른 하나는 1-7로 센다는 점이에요. 이걸 우리가 직접 기억하는 대신 Weekday 열거에 메서드를 추가해 date 인스턴스에서 요일을 뽑아내 대응하는 열거 멤버를 돌려주게 만들 수 있습니다.

@classmethod
def from_date(cls, date):
    return cls(date.isoweekday())

이제 완전한 Weekday 열거는 이렇게 생겼어요.

>>> class Weekday(Enum):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 3
...     THURSDAY = 4
...     FRIDAY = 5
...     SATURDAY = 6
...     SUNDAY = 7
...     #
...     @classmethod
...     def from_date(cls, date):
...         return cls(date.isoweekday())

이제 오늘이 무슨 요일인지 알아낼 수 있어요! 직접 보시죠.

>>> import datetime as dt
>>> Weekday.from_date(dt.date.today())
<Weekday.TUESDAY: 2>

물론 다른 날짜에 이 문서를 읽고 있다면 그 날짜가 보일 거예요.

Weekday 열거는 변수가 하루만 필요할 때는 훌륭하지만, 여러 날이 필요하면 어떨까요? 일주일 동안의 집안일을 그려 주는 함수를 만들고 싶은데 list는 쓰고 싶지 않다고 해 볼게요. 그러면 다른 종류의 Enum을 쓸 수 있습니다.

>>> from enum import Flag
>>> class Weekday(Flag):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 4
...     THURSDAY = 8
...     FRIDAY = 16
...     SATURDAY = 32
...     SUNDAY = 64

두 가지가 바뀌었어요. Flag를 상속했고, 값들이 모두 2의 거듭제곱이라는 점이죠.

원래 Weekday 열거와 마찬가지로 단일 선택도 할 수 있습니다.

>>> first_week_day = Weekday.MONDAY
>>> first_week_day
<Weekday.MONDAY: 1>

하지만 Flag는 여러 멤버를 단일 변수로 결합하는 것도 허용합니다.

>>> weekend = Weekday.SATURDAY | Weekday.SUNDAY
>>> weekend
<Weekday.SATURDAY|SUNDAY: 96>

Flag 변수는 반복(iterate)까지 가능해요.

>>> for day in weekend:
...     print(day)
Weekday.SATURDAY
Weekday.SUNDAY

좋아요, 이제 집안일을 좀 세팅해 볼게요.

>>> chores_for_ethan = {
...     'feed the cat': Weekday.MONDAY | Weekday.WEDNESDAY | Weekday.FRIDAY,
...     'do the dishes': Weekday.TUESDAY | Weekday.THURSDAY,
...     'answer SO questions': Weekday.SATURDAY,
... }

그리고 주어진 날의 집안일을 보여주는 함수를 만듭니다.

>>> def show_chores(chores, day):
...     for chore, days in chores.items():
...         if day in days:
...             print(chore)
...
>>> show_chores(chores_for_ethan, Weekday.SATURDAY)
answer SO questions

멤버의 실제 값이 중요하지 않은 경우에는 수고를 덜 수 있도록 값에 auto()를 쓰면 됩니다.

>>> from enum import auto
>>> class Weekday(Flag):
...     MONDAY = auto()
...     TUESDAY = auto()
...     WEDNESDAY = auto()
...     THURSDAY = auto()
...     FRIDAY = auto()
...     SATURDAY = auto()
...     SUNDAY = auto()
...     WEEKEND = SATURDAY | SUNDAY

열거 멤버와 그 속성에의 프로그래매틱 접근 (Programmatic access)

때로는 열거의 멤버에 프로그래매틱하게(즉, 정확한 색이 프로그램 작성 시점에 알려지지 않아 Color.RED로는 안 되는 상황) 접근하는 것이 유용합니다. Enum은 그런 접근을 허용해요.

>>> Color(1)
<Color.RED: 1>
>>> Color(3)
<Color.BLUE: 3>

멤버를 이름으로 접근하고 싶다면 항목 접근(items access)을 쓰세요.

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

열거 멤버가 있고 그 name이나 value가 필요하다면:

>>> member = Color.RED
>>> member.name
'RED'
>>> member.value
1

열거 멤버와 값의 중복 (Duplicating enum members and values)

같은 이름의 열거 멤버가 두 개 있는 것은 유효하지 않습니다.

>>> class Shape(Enum):
...     SQUARE = 2
...     SQUARE = 3
...
Traceback (most recent call last):
...
TypeError: 'SQUARE' already defined as 2

하지만 열거 멤버 하나에 다른 이름이 연관될 수는 있습니다. 같은 값(그리고 A가 먼저 정의됨)을 가진 두 항목 AB가 있으면, B는 멤버 A의 별칭(alias)입니다. A의 값에 의한 조회는 멤버 A를 돌려주고, 이름에 의한 A 조회도 멤버 A를 돌려줍니다. 이름에 의한 B 조회 역시 멤버 A를 돌려줘요.

>>> class Shape(Enum):
...     SQUARE = 2
...     DIAMOND = 1
...     CIRCLE = 3
...     ALIAS_FOR_SQUARE = 2
...
>>> Shape.SQUARE
<Shape.SQUARE: 2>
>>> Shape.ALIAS_FOR_SQUARE
<Shape.SQUARE: 2>
>>> Shape(2)
<Shape.SQUARE: 2>

참고

이미 정의된 속성(다른 멤버, 메서드 등)과 같은 이름의 멤버를 만들려는 시도, 또는 멤버와 같은 이름의 속성을 만들려는 시도는 허용되지 않습니다.

열거 값의 고유성 보장 (Ensuring unique enumeration values)

기본적으로 열거형은 여러 이름이 같은 값의 별칭이 되는 것을 허용합니다. 이 동작이 바람직하지 않을 때는 @unique 데코레이터를 사용할 수 있어요.

>>> from enum import Enum, unique
>>> @unique
... class Mistake(Enum):
...     ONE = 1
...     TWO = 2
...     THREE = 3
...     FOUR = 3
...
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE

자동 값 사용하기 (Using automatic values)

정확한 값이 중요하지 않다면 auto를 쓸 수 있습니다.

>>> from enum import Enum, auto
>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> [member.value for member in Color]
[1, 2, 3]

값은 _generate_next_value_()에 의해 선택되는데, 이것을 덮어쓸(override) 수 있어요.

>>> class AutoName(Enum):
...     @staticmethod
...     def _generate_next_value_(name, start, count, last_values):
...         return name
...
>>> class Ordinal(AutoName):
...     NORTH = auto()
...     SOUTH = auto()
...     EAST = auto()
...     WEST = auto()
...
>>> [member.value for member in Ordinal]
['NORTH', 'SOUTH', 'EAST', 'WEST']

참고

_generate_next_value_() 메서드는 어떤 멤버보다 먼저 정의되어야 합니다.

반복 (Iteration)

열거의 멤버를 반복해도 별칭은 제공되지 않습니다.

>>> list(Shape)
[<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]
>>> list(Weekday)
[<Weekday.MONDAY: 1>, <Weekday.TUESDAY: 2>, <Weekday.WEDNESDAY: 4>, <Weekday.THURSDAY: 8>, <Weekday.FRIDAY: 16>, <Weekday.SATURDAY: 32>, <Weekday.SUNDAY: 64>]

별칭인 Shape.ALIAS_FOR_SQUAREWeekday.WEEKEND는 보이지 않는 걸 확인할 수 있어요.

특수 속성 __members__는 이름을 멤버에 매핑하는 읽기 전용 순서 매핑입니다. 별칭을 포함해 열거에 정의된 모든 이름을 담고 있어요.

>>> for name, member in Shape.__members__.items():
...     name, member
...
('SQUARE', <Shape.SQUARE: 2>)
('DIAMOND', <Shape.DIAMOND: 1>)
('CIRCLE', <Shape.CIRCLE: 3>)
('ALIAS_FOR_SQUARE', <Shape.SQUARE: 2>)

__members__ 속성은 열거 멤버에 대한 상세한 프로그래매틱 접근에 쓸 수 있습니다. 예를 들어 모든 별칭을 찾는 경우요.

>>> [name for name, member in Shape.__members__.items() if member.name != name]
['ALIAS_FOR_SQUARE']

참고

플래그의 별칭에는 여러 플래그가 설정된 값(예: 3)과 아무 플래그도 설정되지 않은 값(즉 0)이 포함됩니다.

비교 (Comparisons)

열거 멤버는 정체성(identity)으로 비교됩니다.

>>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED is not Color.BLUE
True

열거 값 사이의 순서 비교는 지원되지 않습니다. Enum 멤버는 정수가 아니거든요(하지만 아래의 IntEnum을 보세요).

>>> Color.RED < Color.BLUE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Color' and 'Color'

다만 동등(equality) 비교는 정의되어 있습니다.

>>> Color.BLUE == Color.RED
False
>>> Color.BLUE != Color.RED
True
>>> Color.BLUE == Color.BLUE
True

열거가 아닌 값과의 비교는 항상 같지 않음(not equal)으로 판정됩니다(IntEnum은 다르게 동작하도록 명시적으로 설계되었습니다. 아래 참조).

>>> Color.BLUE == 2
False

경고

모듈을 다시 로드(reload)할 수 있는데, 다시 로드된 모듈에 열거가 들어 있으면 그것들이 재생성되고 새 멤버들은 원래 멤버와 동일(identical/equal)하게 비교되지 않을 수 있습니다.

열거의 허용 멤버와 속성 (Allowed members and attributes)

위 예시의 대부분은 열거 값으로 정수를 사용합니다. 정수를 쓰는 것은 짧고 편리하며(그리고 Functional API에서 기본으로 제공됩니다) 엄격하게 강제되지는 않아요. 압도적인 대부분의 사용 사례에서 열거의 실제 값이 무엇인지는 신경 쓰지 않습니다. 하지만 값이 정말로 중요하다면 열거는 임의의 값을 가질 수 있습니다.

열거는 파이썬 클래스이므로 평소처럼 메서드와 특수 메서드를 가질 수 있습니다. 이런 열거가 있다고 해 볼게요.

>>> class Mood(Enum):
...     FUNKY = 1
...     HAPPY = 3
...
...     def describe(self):
...         # self is the member here
...         return self.name, self.value
...
...     def __str__(self):
...         return 'my custom str! {0}'.format(self.value)
...
...     @classmethod
...     def favorite_mood(cls):
...         # cls here is the enumeration
...         return cls.HAPPY
...

그러면:

>>> Mood.favorite_mood()
<Mood.HAPPY: 3>
>>> Mood.HAPPY.describe()
('HAPPY', 3)
>>> str(Mood.FUNKY)
'my custom str! 1'

무엇이 허용되는지에 대한 규칙은 이렇습니다. 단일 밑줄로 시작하고 끝나는 이름은 enum이 예약해서 쓸 수 없고, 열거 안에 정의된 다른 모든 속성들은 특수 메서드(__str__(), __add__() 등), 디스크립터(메서드도 디스크립터입니다), 그리고 _ignore_에 나열된 변수 이름을 제외하고는 이 열거의 멤버가 됩니다.

참고로 열거가 __new__() 및/또는 __init__()을 정의하면, 열거 멤버에 주어진 어떤 값(들)이 그 메서드들로 전달됩니다. 예시는 Planet을 보세요.

참고

__new__() 메서드는 정의되어 있으면 Enum 멤버 생성 중에 사용되고, 그 뒤에는 클래스 생성 이후 기존 멤버 조회에 사용되는 Enum의 __new__()로 교체됩니다. 자세한 내용은 "__new__() vs. __init__()을 언제 쓸까"를 보세요.

제한된 Enum 서브클래싱 (Restricted Enum subclassing)

Enum 클래스는 기본 열거 클래스 하나, 구체적인 데이터 타입 하나(선택), 그리고 필요한 만큼의 object 기반 mixin 클래스를 가질 수 있습니다. 이 기반 클래스들의 순서는 이렇습니다.

class EnumName([mix-in, ...,] [data-type,] base-enum):
    pass

또한 열거를 서브클래싱하는 것은 열거가 아무 멤버도 정의하지 않을 때만 허용됩니다. 그래서 이건 금지돼요.

>>> class MoreColor(Color):
...     PINK = 17
...
Traceback (most recent call last):
...
TypeError: <enum 'MoreColor'> cannot extend <enum 'Color'>

하지만 이건 허용됩니다.

>>> class Foo(Enum):
...     def some_behavior(self):
...         pass
...
>>> class Bar(Foo):
...     HAPPY = 1
...     SAD = 2
...

멤버를 정의하는 열거의 서브클래싱을 허용하면 타입과 인스턴스의 몇 가지 중요한 불변식(invariant)을 위반하게 됩니다. 반면에 열거 그룹 사이에서 공통 동작 일부를 공유하는 것은 허용하는 편이 이치에 맞아요.(예시는 OrderedEnum 참조.)

Dataclass 지원 (Dataclass support)

dataclass에서 상속할 때 __repr__()은 상속한 클래스의 이름을 생략합니다. 예를 들면:

>>> from dataclasses import dataclass, field
>>> @dataclass
... class CreatureDataMixin:
...     size: str
...     legs: int
...     tail: bool = field(repr=False, default=True)
...
>>> class Creature(CreatureDataMixin, Enum):
...     BEETLE = 'small', 6
...     DOG = 'medium', 4
...
>>> Creature.DOG
<Creature.DOG: size='medium', legs=4>

표준 repr()을 쓰려면 dataclass() 인자 repr=False를 사용하세요.

버전 3.12에서 변경: 값 영역에는 dataclass 필드만 표시되고 dataclass 이름은 표시되지 않습니다.

참고

Enum과 그 서브클래스에 @dataclasses.dataclass 데코레이터를 추가하는 것은 지원되지 않습니다. 오류를 일으키지는 않지만, 멤버들이 서로 같아지는 등 런타임에 아주 이상한 결과를 만들어 냅니다.

>>> @dataclass # don't do this: it does not make any sense
... class Color(Enum):
...     RED = 1
...     BLUE = 2
...
>>> Color.RED is Color.BLUE
False
>>> Color.RED == Color.BLUE  # problem is here: they should not be equal
True

피클링 (Pickling)

열거형은 피클(pickle)하고 언피클(unpickle)할 수 있습니다.

>>> from test.test_enum import Fruit
>>> from pickle import dumps, loads
>>> Fruit.TOMATO is loads(dumps(Fruit.TOMATO))
True

피클링의 평소 제약이 적용됩니다. 피클 가능한 열거는 모듈의 최상위에 정의해야 하는데, 언피클링은 해당 모듈에서 임포트할 수 있어야 하기 때문입니다.

참고

pickle 프로토콜 버전 4에서는 다른 클래스 안에 중첩된 열거를 쉽게 피클할 수 있습니다.

열거 클래스에 __reduce_ex__()를 정의하면 멤버가 피클/언피클되는 방식을 수정할 수 있습니다. 기본 메서드는 값(value) 기준이지만, 복잡한 값을 가진 열거는 이름(name) 기준을 쓰고 싶을 수 있어요.

>>> import enum
>>> class MyEnum(enum.Enum):
...     __reduce_ex__ = enum.pickle_by_enum_name

참고

플래그에는 이름 기준을 권장하지 않습니다. 이름 없는 별칭이 언피클되지 않을 수 있기 때문입니다.

함수형 API (Functional API)

Enum 클래스는 호출 가능해서 다음과 같은 함수형 API를 제공합니다.

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG')
>>> Animal
<enum 'Animal'>
>>> Animal.ANT
<Animal.ANT: 1>
>>> list(Animal)
[<Animal.ANT: 1>, <Animal.BEE: 2>, <Animal.CAT: 3>, <Animal.DOG: 4>]

이 API의 의미는 namedtuple과 비슷합니다. Enum 호출의 첫 번째 인자는 열거의 이름입니다.

두 번째 인자는 열거 멤버 이름의 source입니다. 공백으로 구분된 이름 문자열, 이름의 시퀀스, 키/값 쌍의 2-튜플 시퀀스, 또는 이름을 값에 매핑하는 매핑(예: 딕셔너리)이 될 수 있어요. 마지막 두 옵션은 열거에 임의의 값을 할당할 수 있게 해 주고, 나머지는 1부터 시작하는 증가 정수를 자동으로 할당합니다(start 매개변수로 다른 시작 값을 지정할 수 있어요). Enum에서 파생된 새 클래스가 반환됩니다. 즉, 위의 Animal 할당은 아래와 동등합니다.

>>> class Animal(Enum):
...     ANT = 1
...     BEE = 2
...     CAT = 3
...     DOG = 4
...

시작 숫자를 0이 아니라 1로 기본 설정하는 이유는, 0이 불리언 의미에서 False이지만 기본적으로 열거 멤버는 모두 True로 평가되기 때문입니다.

함수형 API로 만든 열거의 피클링은 까다로울 수 있는데, 열거가 생성되는 모듈을 알아내기 위해 프레임 스택 구현 세부 사항이 사용되기 때문입니다(예를 들어 별도 모듈에서 유틸리티 함수를 쓰면 실패하고, IronPython이나 Jython에서는 동작하지 않을 수도 있어요). 해결책은 다음과 같이 모듈 이름을 명시적으로 지정하는 것입니다.

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', module=__name__)

경고

module이 제공되지 않고 Enum이 그것이 무엇인지 판단할 수 없으면, 새 Enum 멤버는 언피클 불가능해집니다. 오류를 원인에 가깝게 유지하기 위해 피클링이 비활성화됩니다.

새 pickle 프로토콜 4는 일부 상황에서 __qualname__이 pickle이 클래스를 찾을 수 있는 위치에 설정되기를 기대합니다. 예를 들어 클래스를 전역 스코프의 클래스 SomeData 안에서 사용할 수 있게 만든 경우요.

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', qualname='SomeData.Animal')

완전한 시그니처는 이렇습니다.

Enum(
    value='NewEnumName',
    names=<...>,
    *,
    module='...',
    qualname='...',
    type=<mixed-in class>,
    start=1,
)
  • value: 새 enum 클래스가 자신의 이름으로 기록할 값.
  • names: 열거 멤버. 공백 또는 쉼표로 구분된 문자열(다른 지정이 없으면 값은 1부터 시작):
'RED GREEN BLUE' | 'RED,GREEN,BLUE' | 'RED, GREEN, BLUE'

또는 이름의 이터레이터:

['RED', 'GREEN', 'BLUE']

또는 (name, value) 쌍의 이터레이터:

[('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]

또는 매핑:

{'CHARTREUSE': 7, 'SEA_GREEN': 11, 'ROSEMARY': 42}
  • module: 새 enum 클래스를 찾을 수 있는 모듈의 이름.
  • qualname: 모듈 안에서 새 enum 클래스를 찾을 수 있는 위치.
  • type: 새 enum 클래스에 믹스인할 타입.
  • start: 이름만 전달됐을 때 셈을 시작할 숫자.

버전 3.5에서 변경: start 매개변수가 추가되었습니다.

파생 열거 (Derived Enumerations)

IntEnum

제공되는 첫 번째 Enum 변형은 int의 서브클래스이기도 합니다. IntEnum의 멤버는 정수와 비교할 수 있고, 확장해서 서로 다른 타입의 정수 열거도 서로 비교할 수 있어요.

>>> from enum import IntEnum
>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Request(IntEnum):
...     POST = 1
...     GET = 2
...
>>> Shape == 1
False
>>> Shape.CIRCLE == 1
True
>>> Shape.CIRCLE == Request.POST
True

하지만 여전히 표준 Enum 열거와는 비교할 수 없습니다.

>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...
>>> Shape.CIRCLE == Color.RED
False

IntEnum 값은 다른 측면에서도 기대하는 대로 정수처럼 동작합니다.

>>> int(Shape.CIRCLE)
1
>>> ['a', 'b', 'c'][Shape.CIRCLE]
'b'
>>> [i for i in range(Shape.SQUARE)]
[0, 1]

StrEnum

제공되는 두 번째 Enum 변형은 str의 서브클래스이기도 합니다. StrEnum의 멤버는 문자열과 비교할 수 있고, 확장해서 서로 다른 타입의 문자열 열거도 서로 비교할 수 있어요.

버전 3.11에서 추가되었습니다.

IntFlag

다음으로 제공되는 Enum 변형 IntFlagint에 기반합니다. 차이는 IntFlag 멤버가 비트 연산자(&, |, ^, ~)로 결합될 수 있고 그 결과가 가능하면 여전히 IntFlag 멤버라는 점이에요. IntEnum처럼 IntFlag 멤버도 정수이므로 int가 쓰이는 어디든 사용할 수 있습니다.

참고

비트 연산 외의 어떤 연산도 IntFlag 멤버의 IntFlag 자격을 잃게 만듭니다.

유효하지 않은 IntFlag 값을 낳는 비트 연산은 IntFlag 자격을 잃습니다. 자세한 내용은 FlagBoundary를 보세요.

버전 3.6에서 추가되었습니다.

버전 3.11에서 변경되었습니다.

샘플 IntFlag 클래스:

>>> from enum import IntFlag
>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...
>>> Perm.R | Perm.W
<Perm.R|W: 6>
>>> Perm.R + Perm.W
6
>>> RW = Perm.R | Perm.W
>>> Perm.R in RW
True

조합에 이름을 붙이는 것도 가능합니다.

>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...     RWX = 7
...
>>> Perm.RWX
<Perm.RWX: 7>
>>> ~Perm.RWX
<Perm: 0>
>>> Perm(7)
<Perm.RWX: 7>

참고

이름이 붙은 조합은 별칭(alias)으로 간주됩니다. 별칭은 반복 중에는 나타나지 않지만 값 조회에서는 반환될 수 있습니다.

버전 3.11에서 변경되었습니다.

IntFlagEnum 사이의 또 하나 중요한 차이는, 아무 플래그도 설정되지 않으면(값이 0이면) 불리언 평가가 False라는 점입니다.

>>> Perm.R & Perm.X
<Perm: 0>
>>> bool(Perm.R & Perm.X)
False

IntFlag 멤버가 int의 서브클래스이기도 하기 때문에 정수와 결합할 수 있지만(IntFlag 자격은 잃을 수 있습니다):

>>> Perm.X | 4
<Perm.R|X: 5>
>>>
>>> Perm.X + 8
9

참고

부정 연산자 ~는 항상 양수 값을 가진 IntFlag 멤버를 돌려줍니다.

>>> (~Perm.X).value == (Perm.R|Perm.W).value == 6
True

IntFlag 멤버는 반복될 수도 있습니다.

>>> list(RW)
[<Perm.R: 4>, <Perm.W: 2>]

버전 3.11에서 추가되었습니다.

Flag

마지막 변형은 Flag입니다. IntFlag처럼 Flag 멤버는 비트 연산자(&, |, ^, ~)로 결합할 수 있습니다. IntFlag와 달리 다른 어떤 Flag 열거나 int와 결합·비교할 수 없어요. 값을 직접 지정하는 것도 가능하지만, 값으로 auto를 쓰고 Flag가 적절한 값을 고르도록 하는 것을 권장합니다.

버전 3.6에서 추가되었습니다.

IntFlag처럼 Flag 멤버의 조합 결과 아무 플래그도 설정되지 않으면 불리언 평가는 False입니다.

>>> from enum import Flag, auto
>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.RED & Color.GREEN
<Color: 0>
>>> bool(Color.RED & Color.GREEN)
False

개별 플래그는 2의 거듭제곱(1, 2, 4, 8, …) 값을 가져야 하고, 플래그 조합은 그렇지 않습니다.

>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...     WHITE = RED | BLUE | GREEN
...
>>> Color.WHITE
<Color.WHITE: 7>

"플래그 없음" 조건에 이름을 붙여도 그 불리언 값은 바뀌지 않습니다.

>>> class Color(Flag):
...     BLACK = 0
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.BLACK
<Color.BLACK: 0>
>>> bool(Color.BLACK)
False

Flag 멤버도 반복될 수 있습니다.

>>> purple = Color.RED | Color.BLUE
>>> list(purple)
[<Color.RED: 1>, <Color.BLUE: 2>]

버전 3.11에서 추가되었습니다.

참고

대부분의 새 코드에서는 EnumFlag를 강력히 권장합니다. IntEnumIntFlag는(정수와 비교 가능하고, 따라서 이행적으로 다른 무관한 열거와도 비교 가능해서) 열거형의 일부 의미론적 약속을 깨기 때문이에요. IntEnumIntFlagEnumFlag로는 안 될 때, 예를 들어 정수 상수를 열거로 교체하거나 다른 시스템과의 상호운용성을 위해 쓸 때만 사용해야 합니다.

기타 (Others)

IntEnumenum 모듈의 일부지만, 독립적으로 구현하기도 아주 쉽습니다.

class IntEnum(int, ReprEnum):  # or Enum instead of ReprEnum
    pass

이는 파생 열거형이 얼마나 비슷하게 정의될 수 있는지 보여줍니다. 예를 들어 int 대신 float을 믹스인한 FloatEnum이요.

몇 가지 규칙:

  • Enum을 서브클래싱할 때, 위의 IntEnum 예시처럼 mixin 타입은 기반의 시퀀스에서 Enum 클래스 자체보다 앞에 와야 합니다.
  • Mixin 타입은 서브클래스화 가능해야 합니다. 예를 들어 boolrange는 서브클래스화가 불가능해서, mixin 타입으로 쓰면 Enum 생성 중 오류가 발생합니다.
  • Enum은 어떤 타입의 멤버든 가질 수 있지만, 추가 타입을 믹스인하면 모든 멤버가 그 타입(위의 int 같은)의 값을 가져야 합니다. 이 제약은 메서드만 추가하고 다른 타입을 지정하지 않는 mixin에는 적용되지 않습니다.
  • 다른 데이터 타입이 믹스인되면 value 속성은 열거 멤버 자체와 같지는 않지만, 동등하며 같게 비교됩니다.
  • data type__new__()을 정의하는 mixin, 또는 dataclass입니다.
  • %-스타일 포매팅: %s%r은 각각 Enum 클래스의 __str__()__repr__()을 호출합니다. 다른 코드(IntEnum%i%h 같은)는 열거 멤버를 믹스인된 타입으로 취급합니다.
  • 포맷 문자열 리터럴, str.format(), format()은 열거의 __str__() 메서드를 사용합니다.

참고

IntEnum, IntFlag, StrEnum은 기존 상수의 대체품(drop-in replacement)으로 설계되었기 때문에, 그들의 __str__() 메서드는 데이터 타입의 __str__() 메서드로 재설정되었습니다.

__new__() vs. __init__()을 언제 쓸까

Enum 멤버의 실제 값을 커스터마이즈하고 싶을 때는 반드시 __new__()을 사용해야 합니다. 다른 어떤 수정도 __new__()이나 __init__() 어디에든 넣을 수 있지만 __init__()이 선호됩니다.

예를 들어 생성자에 여러 항목을 전달하고 싶지만 그중 하나만 값으로 쓰고 싶다면:

>>> class Coordinate(bytes, Enum):
...     """
...     Coordinate with binary codes that can be indexed by the int code.
...     """
...     def __new__(cls, value, label, unit):
...         obj = bytes.__new__(cls, [value])
...         obj._value_ = value
...         obj.label = label
...         obj.unit = unit
...         return obj
...     PX = (0, 'P.X', 'km')
...     PY = (1, 'P.Y', 'km')
...     VX = (2, 'V.X', 'km/s')
...     VY = (3, 'V.Y', 'km/s')
...
>>> print(Coordinate['PY'])
Coordinate.PY
>>>
>>> print(Coordinate(3))
Coordinate.VY

경고

super().__new__()을 호출하면 안 됩니다. 조회 전용 __new__가 발견되는 쪽이기 때문입니다. 대신 데이터 타입을 직접 사용하세요.

미세한 점들 (Finer Points)

지원되는 __dunder___sunder_ 이름

지원되는 __dunder___sunder_ 이름은 Enum API 문서에서 찾을 수 있습니다.

_Private__names

사설(private) 이름은 열거 멤버로 변환되지 않고 일반 속성으로 남습니다.

버전 3.11에서 변경되었습니다.

Enum 멤버 타입

Enum 멤버는 자신의 enum 클래스의 인스턴스이며, 보통 EnumClass.member로 접근합니다. 커스텀 enum 동작을 작성하는 것 같은 특정 상황에서는 멤버 하나에서 다른 멤버로 직접 접근할 수 있는 것이 유용한데, 이는 지원됩니다. 다만 멤버 이름과 믹스인 클래스의 속성·메서드 사이의 이름 충돌을 피하려면 대문자 이름을 강력히 권장합니다.

버전 3.5에서 변경되었습니다.

다른 데이터 타입과 믹스인한 멤버 만들기

intstr 같은 다른 데이터 타입을 Enum과 함께 서브클래싱할 때, = 뒤의 모든 값은 그 데이터 타입의 생성자로 전달됩니다. 예를 들어:

>>> class MyEnum(IntEnum):  # help(int) -> int(x, base=10) -> integer
...     example = '11', 16  # so x='11' and base=16
...
>>> MyEnum.example.value    # and hex(11) is...
17

Enum 클래스와 멤버의 불리언 값

비-Enum 타입(예: int, str 등)과 믹스인된 Enum 클래스는 믹스인된 타입의 규칙에 따라 평가됩니다. 그 외에는 모든 멤버가 True로 평가돼요. 자신의 enum 불리언 평가가 멤버의 값에 의존하게 하려면 클래스에 다음을 추가하세요.

def __bool__(self):
    return bool(self.value)

순수 Enum 클래스는 항상 True로 평가됩니다.

메서드를 가진 Enum 클래스

아래의 Planet 클래스처럼 enum 서브클래스에 추가 메서드를 주면, 그 메서드는 멤버의 dir()에는 나타나지만 클래스의 dir()에는 나타나지 않습니다.

>>> dir(Planet)
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
>>> dir(Planet.EARTH)
['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value']

Flag 멤버 결합하기

Flag 멤버 조합을 반복하면 단일 비트로 구성된 멤버만 돌려줍니다.

>>> class Color(Flag):
...     RED = auto()
...     GREEN = auto()
...     BLUE = auto()
...     MAGENTA = RED | BLUE
...     YELLOW = RED | GREEN
...     CYAN = GREEN | BLUE
...
>>> Color(3)  # named combination
<Color.YELLOW: 3>
>>> Color(7)  # not named combination
<Color.RED|GREEN|BLUE: 7>

FlagIntFlag의 세부 사항

다음 스니펫을 예시로 사용할게요.

>>> class Color(IntFlag):
...     BLACK = 0
...     RED = 1
...     GREEN = 2
...     BLUE = 4
...     PURPLE = RED | BLUE
...     WHITE = RED | GREEN | BLUE
...

다음은 모두 참입니다.

  • 단일 비트 플래그는 정식(canonical)입니다.
  • 다중 비트 및 0비트 플래그는 별칭입니다.
  • 반복 중에는 정식 플래그만 반환됩니다.
>>> list(Color.WHITE)
[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 4>]
  • 플래그 또는 플래그 집합을 부정하면 대응하는 양수 정수 값을 가진 새 플래그/플래그 집합을 돌려줍니다.
>>> Color.BLUE
<Color.BLUE: 4>
>>>
>>> ~Color.BLUE
<Color.RED|GREEN: 3>
  • 의사-플래그(pseudo-flag)의 이름은 멤버들의 이름으로 구성됩니다.
>>> (Color.RED | Color.GREEN).name
'RED|GREEN'
>>>
>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...
>>> (Perm.R & Perm.W).name is None  # effectively Perm(0)
True
  • 다중 비트 플래그, 즉 별칭은 연산에서 반환될 수 있습니다.
>>> Color.RED | Color.BLUE
<Color.PURPLE: 5>
>>>
>>> Color(7)  # or Color(-1)
<Color.WHITE: 7>
>>>
>>> Color(0)
<Color.BLACK: 0>
  • 포함/멤버십 확인: 0값 플래그는 항상 포함된 것으로 간주됩니다.
>>> Color.BLACK in Color.WHITE
True
  • 그 외에는, 한 플래그의 모든 비트가 다른 플래그에 있을 때만 True가 반환됩니다.
>>> Color.PURPLE in Color.WHITE
True
>>>
>>> Color.GREEN in Color.PURPLE
False

범위 밖/무효 비트를 어떻게 처리할지 제어하는 새 경계(boundary) 메커니즘이 있습니다. STRICT, CONFORM, EJECT, KEEP이 그것입니다.

  • STRICT – 무효 값을 마주하면 예외를 일으킵니다.
  • CONFORM – 무효 비트를 버립니다.
  • EJECT – Flag 상태를 잃고 주어진 값의 일반 int가 됩니다.
  • KEEP – 추가 비트를 유지합니다.
    • Flag 상태와 추가 비트를 유지합니다.
    • 추가 비트는 반복에는 나타나지 않습니다.
    • 추가 비트는 repr()과 str()에는 나타납니다.

Flag의 기본은 STRICT, IntFlag의 기본은 EJECT, _convert_의 기본은 KEEP입니다(KEEP이 필요한 예시는 ssl.Options를 보세요).

Enums와 Flags는 어떻게 다를까?

Enum은 파생된 Enum 클래스와 그 인스턴스(멤버) 둘 다의 여러 측면에 영향을 주는 커스텀 메타클래스를 가집니다.

Enum 클래스

EnumType 메타클래스는 __contains__(), __dir__(), __iter__() 및 일반 클래스에서는 실패하는 일(예: list(Color) 또는 some_enum_var in Color)을 Enum 클래스로 할 수 있게 해 주는 다른 메서드들을 제공합니다. EnumType은 최종 Enum 클래스의 다른 여러 메서드(예: __new__(), __getnewargs__(), __str__(), __repr__())가 올바른지 보장합니다.

Flag 클래스

Flag는 확장된 별칭 보기를 가집니다. 정식(canonical)이 되려면 플래그의 값이 2의 거듭제곱 값이어야 하고 중복 이름이 아니어야 합니다. 그래서 Enum의 별칭 정의에 더해, 값이 없는(일명 0) 플래그나 2의 거듭제곱 값이 두 개 이상인(예: 3) 플래그는 별칭으로 간주됩니다.

Enum 멤버 (일명 인스턴스)

Enum 멤버에 대해 가장 흥미로운 점은 그것들이 싱글턴(singleton)이라는 것입니다. EnumType은 enum 클래스 자체를 만드는 동안 그것들을 모두 만들고, 기존 멤버 인스턴스만 리턴함으로써 새 인스턴스가 절대 만들어지지 않도록 커스텀 __new__()을 제자리에 둡니다.

Flag 멤버

Flag 멤버는 Flag 클래스처럼 반복될 수 있으며, 정식 멤버만 반환됩니다. 예를 들어:

>>> list(Color)
[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 4>]

(BLACK, PURPLE, WHITE는 나타나지 않는다는 점에 주목하세요.)

플래그 멤버를 반전하면 음수 값이 아니라 대응하는 양수 값을 돌려줍니다. 예를 들어:

>>> ~Color.RED
<Color.GREEN|BLUE: 6>

Flag 멤버는 포함한 2의 거듭제곱 값의 수에 해당하는 길이를 가집니다. 예를 들어:

>>> len(Color.PURPLE)
2

Enum 쿡북 (Enum Cookbook)

Enum, IntEnum, StrEnum, Flag, IntFlag가 대부분의 사용 사례를 덮을 것으로 예상되지만, 전부를 덮을 수는 없습니다. 여기 직접 사용하거나 자신만의 것을 만드는 예시로 쓸 수 있는 몇 가지 다른 타입의 열거 레시피가 있습니다.

값 생략하기 (Omitting values)

많은 사용 사례에서 열거의 실제 값이 무엇인지는 신경 쓰지 않습니다. 이런 단순 열거형을 정의하는 방법은 여러 가지가 있어요.

  • 값에 auto의 인스턴스를 사용
  • 값에 object의 인스턴스를 사용
  • 설명적인 문자열을 값으로 사용
  • 튜플을 값으로 사용하고 커스텀 __new__()으로 그 튜플을 int 값으로 교체

이 중 어떤 방법을 쓰든 사용자에게 이 값들이 중요하지 않다는 것을 알리고, 나머지 멤버의 번호를 다시 매기지 않고도 멤버를 추가·제거·재정렬할 수 있게 해 줍니다.

auto 사용하기

auto를 사용하면 이렇게 생깁니다.

>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.GREEN
<Color.GREEN: 3>

object 사용하기

object를 사용하면 이렇게 생깁니다.

>>> class Color(Enum):
...     RED = object()
...     GREEN = object()
...     BLUE = object()
...
>>> Color.GREEN
<Color.GREEN: <object object at 0x...>>

이건 직접 __repr__()을 작성하고 싶은 이유를 보여 주는 좋은 예시이기도 합니다.

>>> class Color(Enum):
...     RED = object()
...     GREEN = object()
...     BLUE = object()
...     def __repr__(self):
...         return "<%s.%s>" % (self.__class__.__name__, self._name_)
...
>>> Color.GREEN
<Color.GREEN>

설명적인 문자열 사용하기

값으로 문자열을 사용하면 이렇게 생깁니다.

>>> class Color(Enum):
...     RED = 'stop'
...     GREEN = 'go'
...     BLUE = 'too fast!'
...
>>> Color.GREEN
<Color.GREEN: 'go'>

커스텀 __new__() 사용하기

자동 번호 __new__()을 사용하면 이렇게 생깁니다.

>>> class AutoNumber(Enum):
...     def __new__(cls):
...         value = len(cls.__members__) + 1
...         obj = object.__new__(cls)
...         obj._value_ = value
...         return obj
...
>>> class Color(AutoNumber):
...     RED = ()
...     GREEN = ()
...     BLUE = ()
...
>>> Color.GREEN
<Color.GREEN: 2>

더 범용적인 AutoNumber를 만들려면 시그니처에 *args를 추가하세요.

>>> class AutoNumber(Enum):
...     def __new__(cls, *args):  # this is the only change from above
...         value = len(cls.__members__) + 1
...         obj = object.__new__(cls)
...         obj._value_ = value
...         return obj
...

그런 다음 AutoNumber에서 상속할 때 자신만의 __init__을 작성해 추가 인자를 처리할 수 있습니다.

>>> class Swatch(AutoNumber):
...     def __init__(self, pantone='unknown'):
...         self.pantone = pantone
...     AUBURN = '3497'
...     SEA_GREEN = '1246'
...     BLEACHED_CORAL = ()  # New color, no Pantone code yet!
...
>>> Swatch.SEA_GREEN
<Swatch.SEA_GREEN: 2>
>>> Swatch.SEA_GREEN.pantone
'1246'
>>> Swatch.BLEACHED_CORAL.pantone
'unknown'

참고

__new__() 메서드는 정의되어 있으면 Enum 멤버 생성 중에 사용되고, 그 뒤에는 클래스 생성 이후 기존 멤버 조회에 사용되는 Enum의 __new__()로 교체됩니다.

경고

super().__new__()을 호출하면 안 됩니다. 조회 전용 __new__가 발견되는 쪽이기 때문입니다. 대신 데이터 타입을 직접 사용하세요. 예:

obj = int.__new__(cls, value)

OrderedEnum

IntEnum에 기반하지 않아서 보통의 Enum 불변식(다른 열거와 비교 불가능 같은)을 유지하는 순서 있는 열거형:

>>> class OrderedEnum(Enum):
...     def __ge__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value >= other.value
...         return NotImplemented
...     def __gt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value > other.value
...         return NotImplemented
...     def __le__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value <= other.value
...         return NotImplemented
...     def __lt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value < other.value
...         return NotImplemented
...
>>> class Grade(OrderedEnum):
...     A = 5
...     B = 4
...     C = 3
...     D = 2
...     F = 1
...
>>> Grade.C < Grade.A
True

DuplicateFreeEnum

별칭을 만드는 대신 중복 멤버 값을 찾으면 오류를 일으킵니다.

>>> class DuplicateFreeEnum(Enum):
...     def __init__(self, *args):
...         cls = self.__class__
...         if any(self.value == e.value for e in cls):
...             a = self.name
...             e = cls(self.value).name
...             raise ValueError(
...                 "aliases not allowed in DuplicateFreeEnum: %r --> %r"
...                 % (a, e))
...
>>> class Color(DuplicateFreeEnum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...     GRENE = 2
...
Traceback (most recent call last):
...
ValueError: aliases not allowed in DuplicateFreeEnum: 'GRENE' --> 'GREEN'

참고

이것은 별칭을 금지하는 것뿐 아니라 다른 동작을 추가·변경하기 위해 Enum을 서브클래싱하는 유용한 예시입니다. 원하는 변경이 별칭 금지뿐이라면 unique() 데코레이터를 대신 쓸 수 있어요.

MultiValueEnum

멤버 하나당 값이 여러 개인 것을 지원합니다.

>>> class MultiValueEnum(Enum):
...     def __new__(cls, value, *values):
...         self = object.__new__(cls)
...         self._value_ = value
...         for v in values:
...             self._add_value_alias_(v)
...         return self
...
>>> class DType(MultiValueEnum):
...     float32 = 'f', 8
...     double64 = 'd', 9
...
>>> DType('f')
<DType.float32: 'f'>
>>> DType(9)
<DType.double64: 'd'>

Planet

__new__()이나 __init__()이 정의되어 있으면 열거 멤버의 값이 그 메서드들로 전달됩니다.

>>> class Planet(Enum):
...     MERCURY = (3.303e+23, 2.4397e6)
...     VENUS = (4.869e+24, 6.0518e6)
...     EARTH = (5.976e+24, 6.37814e6)
...     MARS = (6.421e+23, 3.3972e6)
...     JUPITER = (1.9e+27, 7.1492e7)
...     SATURN = (5.688e+26, 6.0268e7)
...     URANUS = (8.686e+25, 2.5559e7)
...     NEPTUNE = (1.024e+26, 2.4746e7)
...     def __init__(self, mass, radius):
...         self.mass = mass       # in kilograms
...         self.radius = radius   # in meters
...     @property
...     def surface_gravity(self):
...         # universal gravitational constant (m3 kg-1 s-2)
...         G = 6.67300E-11
...         return G * self.mass / (self.radius * self.radius)
...
>>> Planet.EARTH.value
(5.976e+24, 6378140.0)
>>> Planet.EARTH.surface_gravity
9.802652743337129

TimePeriod

_ignore_ 속성이 사용되는 예시입니다.

>>> import datetime as dt
>>> class Period(dt.timedelta, Enum):
...     "different lengths of time"
...     _ignore_ = 'Period i'
...     Period = vars()
...     for i in range(367):
...         Period['day_%d' % i] = i
...
>>> list(Period)[:2]
[<Period.day_0: datetime.timedelta(0)>, <Period.day_1: datetime.timedelta(days=1)>]
>>> list(Period)[-2:]
[<Period.day_365: datetime.timedelta(days=365)>, <Period.day_366: datetime.timedelta(days=366)>]

EnumType 서브클래싱 (Subclassing EnumType)

대부분의 enum 요구는 Enum 서브클래스를 클래스 데코레이터나 커스텀 함수로 커스터마이징해서 충족될 수 있지만, EnumType을 서브클래싱해서 다른 Enum 경험을 제공할 수도 있습니다.

더 알아보기 (Learn more)