견고한 Python - 4일차 User-Defined Types: Enums

0

robust_python

목록 보기
4/5

User-Defined Types: Enums

1. Enumerations

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문자열이 들어가서는 안된다. 이는 오탈자로 발생하는 문제를 해결해주며, 더 쉽게 함수를 접근할 수 있게해준다.

2. Advanced Usage

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'

그러나 LiteralEnum과 같이 여러가지 기능들을 제공하지 않기 때문에 간단히 사용할 때는 Literal을 사용하고, 만약 iteration과 runtime checking, 다른 값 할당이 필요하다면 Enum을 사용하는 것이 좋다.

3. Flags

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.

allergensAllergen.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는 견과류와 관련된 알러지를 표현한다.

4. Integer Conversion

재밌는 것은 Enum이든 Flag든 숫자값을 가진 데이터라도 숫자로 비교가 불가능하다는 것이다. 다음의 예시를 보도록 하자.

from enum import Enum
class ImperialLiquidMeasure(Enum):
    CUP = 8
    PINT = 16
    QUART = 32
    GALLON = 128
    
print(ImperialLiquidMeasure.CUP == 8) # False

Enum을 받은 ImperialLiquidMeasureCUP8과 호환되지 않는다. 이는 Enum으로 만든 값들은 일반 값들과 호환되지 않는다는 것을 알려준다.

그런데, 만약 호환되기를 원한다면 IntEnum을 사용하면 된다.

from enum import IntEnum
class ImperialLiquidMeasure(IntEnum):
    CUP = 8
    PINT = 16
    QUART = 32
    GALLON = 128
    
print(ImperialLiquidMeasure.CUP == 8) # True

호환 가능한 것을 볼 수 있다. IntEnum뿐만 아니라 IntFlag 역시도 마찬가지의 동작을 한다. 그런데, 이는 추천하지 않은 동작이다. 이처럼 약한 타입 관계는 위험한 코드를 만들 가능성을 높인다. 가령, 어떤 상수는 WATER8인데 CUP8이라서 서로 호환된다고하면 위험한 코드를 만들 수 있다.

따라서, IntEnumIntFlag는 왠만한 상황 아니고서는 사용하지 말도록 하자.

5. Unique

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

이는 같은 값을 가진 서로 다른 이름의 데이터가 있기 때문이다. 하나를 지우고 다시 구동해보면 성공하는 것을 확인할 수 있다.

0개의 댓글