python3.4부터 enumeration이 제공되었는데, 이전까지는 대문자로 변수를 선언하여 상수처럼 사용했다.
BÉCHAMEL = "Béchamel"
VELOUTÉ = "Velouté"
ESPAGNOLE = "Espagnole"
TOMATO = "Tomato"
HOLLANDAISE = "Hollandaise"
MOTHER_SAUCES = (BÉCHAMEL, VELOUTÉ, ESPAGNOLE, TOMATO, HOLLANDAISE)
문제는 이러한 상수들을 사용하도록 강제할 방법이 없었다는 것이다. 때문에 몇 달 후에 다시 코드를 보면, 해당 상수가 어디에 쓰이는 지도 헷갈리고, 함수를 사용할 때 어떤 상수를 입력해야하는 지도 헷갈리게 된다.
이를 해결하기위해 Enum
이 사용되는 것이다. python의 Enum
은 다음과 같다.
from enum import Enum
class MotherSauce(Enum):
BÉCHAMEL = "Béchamel"
VELOUTÉ = "Velouté"
ESPAGNOLE = "Espagnole"
TOMATO = "Tomato"
HOLLANDAISE = "Hollandaise"
print(MotherSauce.BÉCHAMEL) # MotherSauce.BÉCHAME
print(MotherSauce("Béchamel")) # MotherSauce.BÉCHAME
클래스에서 Enum
을 상속받아, static한 클래스 변수들을 만들어주며 된다. static하기 때문에 MotherSauce.BÉCHAMEL
처럼 class 자체로 접근이 가능하다. 또한, MotherSauce("Béchamel")
으로 인스턴스화 시키면 설정한 static 변수 값인 MotherSauce.BÉCHAMEL
가 나온다.
만약 Enum
으로 정의하지 않은 data를 입력하면 에러가 발생한다. 가령 MotherSauce("GYU")
이렇게 작성하면 GYU
는 MotherSauce`에 없기 때문에 에러가 발생한다.
print(MotherSauce("GYU"))
ValueError: 'GYU' is not a valid MotherSauce
Enum
으로 만들어진 클래스는 iterate될 수 있는데 재밌게도 enumerate
를 사용하면 되므로, 따로 list로 static data 변수들을 묶어낼 필요가 없다.
for i, sauce in enumerate(MotherSauce, start=1):
print(i, ":th, sause:",sauce)
# 1 :th, sause: MotherSauce.BÉCHAMEL
# 2 :th, sause: MotherSauce.VELOUTÉ
# 3 :th, sause: MotherSauce.ESPAGNOLE
# 4 :th, sause: MotherSauce.TOMATO
# 5 :th, sause: MotherSauce.HOLLANDAISE
이제 Enum
을 통해서 변수에 타입을 줄 수 있게 된 것이다. 이렇게 됨으로서 잘못된 const값을 주지 않게 만들 수 있는 것이다.
def create_sauce(mother_sauce: MotherSauce, extra_ingredients: list[str]):
# ...
mother_sauce
는 반드시 MotherSauce
Enum타입이 들어가야 하며 그냥 str
문자열이 들어가서는 안된다. 이는 오탈자로 발생하는 문제를 해결해주며, 더 쉽게 함수를 접근할 수 있게해준다.
enum을 사용할 때, 사실 값이 중요하지 않은 경우들이 있다. 이런 경우 auto()
함수를 사용하면 쉽게 1부터 시작해서 값을 바인딩해준다.
from enum import auto, Enum
class MotherSauce(Enum):
BÉCHAMEL = auto()
VELOUTÉ = auto()
ESPAGNOLE = auto()
TOMATO = auto()
HOLLANDAISE = auto()
print(list(MotherSauce))
# [<MotherSauce.BÉCHAMEL: 1>, <MotherSauce.VELOUTÉ: 2>, <MotherSauce.ESPAGNOLE: 3>, <MotherSauce.TOMATO: 4>, <MotherSauce.HOLLANDAISE: 5>]
기본적으로 auto()
는 자동적으로 1부터 1씩 증가한 값들을 할당해준다. 만약, 자동으로 할당되는 값을 제어하고 싶다면 _generate_next_value_()
함수를 사용하면 된다.
from enum import auto, Enum
class MotherSauce(Enum):
def _generate_next_value_(name, start, count, last_values):
return name.capitalize()
BÉCHAMEL = auto()
VELOUTÉ = auto()
ESPAGNOLE = auto()
TOMATO = auto()
HOLLANDAISE = auto()
print(list(MotherSauce))
# [<MotherSauce.BÉCHAMEL: 'Béchamel'>, <MotherSauce.VELOUTÉ: 'Velouté'>, <MotherSauce.ESPAGNOLE: 'Espagnole'>, <MotherSauce.TOMATO: 'Tomato'>, <MotherSauce.HOLLANDAISE: 'Hollandaise'>]
다음은 _generate_next_value_
을 사용하여 대문자의 이름을 자동 할당하도록 한 코드이다.
python의 Literal
은 python3.8에 도입되었고 Enum
과 같이 자동으로 값을 설정해주지만, 값이 별로 중요하지 않은 경우에 사용된다.
sauce: Literal['Béchamel', 'Velouté', 'Espagnole',
'Tomato', 'Hollandaise'] = 'Hollandaise'
그러나 Literal
은 Enum
과 같이 여러가지 기능들을 제공하지 않기 때문에 간단히 사용할 때는 Literal
을 사용하고, 만약 iteration과 runtime checking, 다른 값 할당이 필요하다면 Enum
을 사용하는 것이 좋다.
Enum을 사용할 때 조합을 이루고 싶을 때가 있다. 가령, 음식의 알러지를 표시하기 위해서 다음과 같이 Allergen
enum을 만든다고 하자.
from enum import auto, Enum
from typing import Set
class Allergen(Enum):
FISH = auto()
SHELLFISH = auto()
TREE_NUTS = auto()
PEANUTS = auto()
GLUTEN = auto()
SOY = auto()
DAIRY = auto()
그런데 사람들은 알러지가 없을 수도 있고, 있을 수도 있는데 있으면 1개 이상이 있을 수 있다. 가령 FISH
도 알러지가 있고, SHELLFISH
도 알러지가 있을 수 있다. 이를 한번에 표현하는 방법으로 c/c++에서는 bitwise연산을 자주 사용한다.
이를 자동으로 해주는 것이 바로 enum
모듈의 Flag
이다.
from enum import Flag, auto
class Allergen(Flag):
FISH = auto()
SHELLFISH = auto()
TREE_NUTS = auto()
PEANUTS = auto()
GLUTEN = auto()
SOY = auto()
DAIRY = auto()
이렇게 Flag
를 클래스에서 상속하도록 하게하면 자동으로 bitwise연산이 가능하도록 값을 설정해준다. 따라서 다음과 같이 표현이 가능하다.
from enum import Flag, auto
class Allergen(Flag):
FISH = auto()
SHELLFISH = auto()
TREE_NUTS = auto()
PEANUTS = auto()
GLUTEN = auto()
SOY = auto()
DAIRY = auto()
allergens = Allergen.FISH | Allergen.SHELLFISH
print(allergens) # Allergen.SHELLFISH|FISH
if allergens & Allergen.FISH:
print("This recipe contains fish.") # This recipe contains fish.
allergens
은 Allergen.FISH | Allergen.SHELLFISH
인데, 이는 01 | 10
으로 bit연산을 했기 때문에 11
이다. 따라서 3이 될 것이다. 이를 bit연산으로 &
을 쓰면 확인가능한 것이다.
이를 이용하여 다음과 같이 조합해볼 수도 있다.
class Allergen(Flag):
FISH = auto()
SHELLFISH = auto()
TREE_NUTS = auto()
PEANUTS = auto()
GLUTEN = auto()
SOY = auto()
DAIRY = auto()
SEAFOOD = Allergen.FISH | Allergen.SHELLFISH
ALL_NUTS = Allergen.TREE_NUTS | Allergen.PEANUTS
SEAFOOD
는 물고기와 관련된 알러지를 ALL_NUTS
는 견과류와 관련된 알러지를 표현한다.
재밌는 것은 Enum
이든 Flag
든 숫자값을 가진 데이터라도 숫자로 비교가 불가능하다는 것이다. 다음의 예시를 보도록 하자.
from enum import Enum
class ImperialLiquidMeasure(Enum):
CUP = 8
PINT = 16
QUART = 32
GALLON = 128
print(ImperialLiquidMeasure.CUP == 8) # False
Enum
을 받은 ImperialLiquidMeasure
의 CUP
는 8
과 호환되지 않는다. 이는 Enum
으로 만든 값들은 일반 값들과 호환되지 않는다는 것을 알려준다.
그런데, 만약 호환되기를 원한다면 IntEnum
을 사용하면 된다.
from enum import IntEnum
class ImperialLiquidMeasure(IntEnum):
CUP = 8
PINT = 16
QUART = 32
GALLON = 128
print(ImperialLiquidMeasure.CUP == 8) # True
호환 가능한 것을 볼 수 있다. IntEnum
뿐만 아니라 IntFlag
역시도 마찬가지의 동작을 한다. 그런데, 이는 추천하지 않은 동작이다. 이처럼 약한 타입 관계는 위험한 코드를 만들 가능성을 높인다. 가령, 어떤 상수는 WATER
가 8
인데 CUP
가 8
이라서 서로 호환된다고하면 위험한 코드를 만들 수 있다.
따라서, IntEnum
과 IntFlag
는 왠만한 상황 아니고서는 사용하지 말도록 하자.
enum에서의 데이터는 키는 달라도 값은 같을 수 있다. 이 경우 같은 enum의 같은 값이기 때문에 동일한 것으로 평가된다. 즉, 동일한 값으로 본다.
from enum import Enum
class MotherSauce(Enum):
BÉCHAMEL = "Béchamel"
BECHAMEL = "Béchamel"
VELOUTÉ = "Velouté"
ESPAGNOLE = "Espagnole"
TOMATO = "Tomato"
HOLLANDAISE = "Hollandaise"
print(MotherSauce.BECHAMEL == MotherSauce.BÉCHAMEL) # True
다음의 MotherSauce
enum은 BÉCHAMEL = "Béchamel"
와 BECHAMEL = "Béchamel"
을 가지고 있다. 이들은 다른 이름의 같은 값을 가지고 있다. 따라서 MotherSauce.BECHAMEL == MotherSauce.BÉCHAMEL
다음의 결과가 서로 같다는 결과인 True
가 나오는 것이다. 물론, MotherSauce.BECHAMEL == "Béchamel"
는 False
이다.
이는 의도할 수 있는 결과일 수 있지만, 보기 좋지도 않을 뿐더러 디버깅할 때도 혼란을 준다. 따라서, enum을 만들 때 uniqueness를 부여하고 싶다면 @unique
annotation을 붙이는 것이 좋다.
from enum import Enum, unique
@unique
class MotherSauce(Enum):
BÉCHAMEL = "Béchamel"
BECHAMEL = "Béchamel"
VELOUTÉ = "Velouté"
ESPAGNOLE = "Espagnole"
TOMATO = "Tomato"
HOLLANDAISE = "Hollandaise"
print(MotherSauce.BECHAMEL == MotherSauce.BÉCHAMEL) # True
다음의 코드를 구동하면 에러가 발생한다.
ValueError: duplicate values found in <enum 'MotherSauce'>: BECHAMEL -> BÉCHAME
이는 같은 값을 가진 서로 다른 이름의 데이터가 있기 때문이다. 하나를 지우고 다시 구동해보면 성공하는 것을 확인할 수 있다.