PEP 435 – Python 표준 라이브러리에 Enum 타입 추가하기
- Author:
- Barry Warsaw <barry at python.org>, Eli Bendersky <eliben at gmail.com>, Ethan Furman <ethan at stoneleaf.us>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 23-Feb-2013
- Python-Version:
- 3.4
- Post-History:
- 23-Feb-2013, 02-May-2013
- Replaces:
- 354
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Python 표준 라이브러리에 열거형 타입을 추가할 것을 제안합니다.
열거형은 고유한 상수 값에 바인딩된 기호 이름의 집합입니다. 열거형 안에서 값은 동일성(identity)으로 비교할 수 있으며, 열거형 자체를 순회할 수 있습니다.
논의 현황
Python에 enum 타입을 추가하자는 아이디어는 새로운 것이 아닙니다. PEP 354는 2005년에 거부된 이전 시도입니다. 최근 python-ideas 메일링 리스트에서 새로운 논의가 시작되었습니다 [3]. 여러 스레드에서 많은 새로운 아이디어가 제안되었고, 긴 논의 끝에 Guido는 flufl.enum을 표준 라이브러리에 추가할 것을 제안했습니다 [4]. PyCon 2013 언어 서밋에서 이 문제가 더 논의되었습니다. 많은 개발자가 int의 서브클래스인 enum을 원한다는 점이 분명해졌습니다. 그런 enum이 있으면 하위 호환성을 포기하지 않고도 표준 라이브러리의 많은 정수 상수를 친숙한 문자열 표현을 갖춘 enum으로 대체할 수 있습니다. 관심 있는 몇몇 코어 개발자 사이의 추가 논의를 통해 IntEnum을 Enum의 특수한 경우로 두자는 제안이 나왔습니다.
Enum과 IntEnum을 가르는 핵심 쟁점은 정수와의 비교가 의미상 타당한지 여부입니다. 열거형의 대부분 용도에서는 정수와의 비교를 거부하는 것이 기능입니다. 정수와 비교되는 enum은 추이성에 의해 서로 관련 없는 타입의 enum 간 비교로 이어지며, 이는 대부분의 경우 바람직하지 않습니다. 그러나 일부 용도에서는 정수와의 더 높은 상호 운용성이 요구됩니다. 예를 들어, 기존 표준 라이브러리 상수(socket.AF_INET 등)를 열거형으로 대체하는 경우가 이에 해당합니다.
2013년 4월 말에 이어진 논의에서 열거형 멤버는 자신이 속한 enum의 타입에 속해야 한다는 결론이 났습니다: type(Color.red) == Color. Guido는 이 문제에 대한 결정 [5]과 함께, 열거형 멤버를 정의하지 않은 경우 [7]를 제외하고는 열거형의 서브클래스를 만드는 것을 허용하지 않는다는 관련 문제 [6]에 대해서도 결정을 공표했습니다.
이 PEP는 2013년 5월 10일에 Guido에 의해 수락되었습니다 [1].
동기
[부분적으로 PEP 354에 기술된 동기에 기반합니다]
열거형의 속성은 의미상의 뜻을 가질 수도 있고 그렇지 않을 수도 있는, 불변이며 서로 관련된 상수 값 집합을 정의하는 데 유용합니다. 전형적인 예로는 요일(일요일부터 토요일까지)과 학교 평가 등급(‘A’부터 ‘D’, 그리고 ‘F’)이 있습니다. 다른 예로는 오류 상태 값과 정의된 프로세스 내의 상태가 있습니다.
이산적인 임의의 값을 나타내기 위해 int나 str 같은 다른 기본 타입의 값 시퀀스를 단순히 정의할 수도 있습니다. 그러나 열거형은 그러한 값이 다른 모든 값, 특히 중요하게는 다른 열거형 안의 값과도 구별되도록 보장하며, 의미 없는 연산(“수요일 곱하기 2”)이 이 값들에 대해 정의되지 않도록 보장합니다. 또한 열거형은 정의할 때 지루한 반복(즉, GREEN = 'green' 같은 것)을 요구하지 않으면서도 enum 값의 편리한 출력용 표현을 제공합니다.
모듈 및 타입 이름
표준 라이브러리에 enum이라는 이름의 모듈을 추가할 것을 제안합니다. 이 모듈이 노출하는 주요 타입은 Enum입니다. 따라서 Enum 타입을 임포트하려면 사용자 코드는 다음을 실행합니다:
>>> from enum import Enum
새로운 열거형 타입에 대해 제안하는 의미 체계
Enum 만들기
열거형은 클래스 문법으로 만들기 때문에 읽고 쓰기가 쉽습니다. 다른 생성 방법은 Functional API에서 설명합니다. 열거형을 정의하려면 다음과 같이 Enum의 서브클래스를 만드십시오.:
>>> from enum import Enum
>>> class Color(Enum):
... red = 1
... green = 2
... blue = 3
용어에 관한 참고 사항: Color를 열거형(또는 enum)이라고 부르며, Color.red, Color.green은 열거형 멤버(또는 enum 멤버)입니다. 열거형 멤버에는 값도 있습니다(Color.red의 값은 1 등입니다).
열거형 멤버에는 사람이 읽을 수 있는 문자열 표현이 있습니다.:
>>> print(Color.red)
Color.red
…반면 repr에는 더 많은 정보가 담겨 있습니다.:
>>> print(repr(Color.red))
<Color.red: 1>
열거형 멤버의 타입은 그 멤버가 속한 열거형입니다.:
>>> type(Color.red)
<Enum 'Color'>
>>> isinstance(Color.green, Color)
True
>>>
열거형에는 항목 이름만 담는 속성도 있습니다.:
>>> print(Color.red.name)
red
열거형은 정의된 순서대로 이터레이션을 지원합니다.:
>>> class Shake(Enum):
... vanilla = 7
... chocolate = 4
... cookies = 9
... mint = 3
...
>>> for shake in Shake:
... print(shake)
...
Shake.vanilla
Shake.chocolate
Shake.cookies
Shake.mint
열거형 멤버는 해시 가능하므로 딕셔너리와 집합에 사용할 수 있습니다.:
>>> apples = {}
>>> apples[Color.red] = 'red delicious'
>>> apples[Color.green] = 'granny smith'
>>> apples
{<Color.red: 1>: 'red delicious', <Color.green: 2>: 'granny smith'}
열거형 멤버에 프로그래밍 방식으로 접근하기
때로는 열거형의 멤버에 프로그래밍 방식으로 접근하는 것이 유용합니다(즉, 프로그램을 작성하는 시점에 정확한 색을 알 수 없어서 Color.red를 쓸 수 없는 상황입니다). Enum은 이러한 접근을 허용합니다.:
>>> Color(1)
<Color.red: 1>
>>> Color(3)
<Color.blue: 3>
enum 멤버에 이름으로 접근하려면 항목 접근을 사용하십시오.:
>>> Color['red']
<Color.red: 1>
>>> Color['green']
<Color.green: 2>
enum 멤버와 값의 중복
이름이 같은 enum 멤버가 두 개 있는 것은 유효하지 않습니다.:
>>> class Shape(Enum):
... square = 2
... square = 3
...
Traceback (most recent call last):
...
TypeError: Attempted to reuse key: square
그러나 두 enum 멤버가 같은 값을 가지는 것은 허용됩니다. 같은 값을 가진 두 멤버 A와 B가 있고 A가 먼저 정의되었다면, B는 A의 별칭입니다. A와 B의 값으로 값 기반 조회를 하면 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>
enum의 멤버를 이터레이션해도 별칭은 나오지 않습니다.:
>>> list(Shape)
[<Shape.square: 2>, <Shape.diamond: 1>, <Shape.circle: 3>]
특수 속성 __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']
비교
열거형 멤버는 동일성으로 비교됩니다.:
>>> Color.red is Color.red
True
>>> Color.red is Color.blue
False
>>> Color.red is not Color.blue
True
열거형 값 사이의 순서 비교는 지원되지 않습니다. 열거형은 정수가 아닙니다(단, 아래의 IntEnum을 참조하십시오).:
>>> Color.red < Color.blue
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: Color() < Color()
하지만 동등 비교는 정의되어 있습니다.:
>>> Color.blue == Color.red
False
>>> Color.blue != Color.red
True
>>> Color.blue == Color.blue
True
열거형이 아닌 값과 비교하면 항상 같지 않은 것으로 비교됩니다(다시 말하지만, IntEnum은 다르게 동작하도록 명시적으로 설계되었습니다. 아래를 참조하십시오).:
>>> Color.blue == 2
False
열거형에서 허용되는 멤버와 속성
위의 예제에서는 열거형 값으로 정수를 사용합니다. 정수를 사용하면 간결하고 편리하지만(그리고 Functional API가 기본으로 제공하는 방식이지만), 반드시 그래야 하는 것은 아닙니다. 대부분의 사용 사례에서는 열거형의 실제 값이 무엇인지 신경 쓰지 않습니다. 하지만 값이 정말로 중요하다면, 열거형은 임의의 값을 가질 수 있습니다.
열거형은 Python 클래스이며, 평소처럼 메서드와 특수 메서드를 가질 수 있습니다. 다음과 같은 열거형이 있다면:
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'
허용되는 것에 대한 규칙은 다음과 같습니다. 열거형 안에 정의된 모든 속성은 이 열거형의 멤버가 되며, __dunder__ 이름과 디스크립터 [9]는 예외입니다. 메서드도 디스크립터입니다.
열거형의 제한된 서브클래싱
열거형의 서브클래싱은 해당 열거형이 멤버를 하나도 정의하지 않은 경우에만 허용됩니다. 따라서 다음은 금지됩니다:
>>> class MoreColor(Color):
... pink = 17
...
TypeError: Cannot extend enumerations
하지만 다음은 허용됩니다:
>>> class Foo(Enum):
... def some_behavior(self):
... pass
...
>>> class Bar(Foo):
... happy = 1
... sad = 2
...
이 결정의 근거는 Guido가 [6]에서 제시했습니다. 멤버를 정의한 열거형의 서브클래스를 만드는 것을 허용하면 타입과 인스턴스의 몇 가지 중요한 불변 조건을 위반하게 됩니다. 반면에, 여러 열거형이 공통 동작을 일부 공유할 수 있게 하는 것은 타당하며, 빈 열거형의 서브클래싱은 IntEnum을 구현하는 데에도 사용됩니다.
IntEnum
int의 서브클래스이기도 한 Enum의 변형을 제안합니다. 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]
대부분의 코드에서는 Enum을 강력히 권장합니다. IntEnum은 (정수와 비교할 수 있고, 따라서 추이성에 의해 관련 없는 다른 열거형과도 비교할 수 있게 되므로) 열거형의 몇 가지 의미론적 약속을 깨뜨리기 때문입니다. 이것은 다른 선택지가 없는 특수한 경우에만 사용해야 합니다. 예를 들어, 정수 상수를 열거형으로 대체하면서 여전히 정수를 기대하는 코드와의 하위 호환성이 필요한 경우입니다.
기타 파생 열거형
IntEnum은 enum 모듈의 일부가 될 것입니다. 하지만 독립적으로 구현하는 것도 매우 간단합니다:
class IntEnum(int, Enum):
pass
이는 비슷한 파생 열거형을 정의하는 방법을 보여 줍니다. 예를 들어 int 대신 str을 혼합하는 StrEnum을 정의할 수 있습니다.
몇 가지 규칙은 다음과 같습니다.
- Enum을 서브클래싱할 때는 위의
IntEnum예제처럼 베이스 시퀀스에서 믹스인 타입이 Enum 자체보다 앞에 나와야 합니다. - Enum은 어떤 타입의 멤버든 가질 수 있지만, 추가 타입을 혼합하면 모든 멤버의 값이 그 타입이어야 합니다. 위의
int가 그 예입니다. 이 제약은 메서드만 추가하고int나str같은 다른 데이터 타입을 지정하지 않는 믹스인에는 적용되지 않습니다.
피클링
열거형은 피클링하고 언피클링할 수 있습니다:
>>> from enum.tests.fruit import Fruit
>>> from pickle import dumps, loads
>>> Fruit.tomato is loads(dumps(Fruit.tomato))
True
피클링에 대한 일반적인 제약이 적용됩니다. 언피클링하려면 해당 모듈에서 열거형을 임포트할 수 있어야 하므로, 피클링 가능한 열거형은 모듈의 최상위 수준에서 정의해야 합니다.
함수형 API
Enum 클래스는 호출 가능 객체이며, 다음과 같은 함수형 API를 제공합니다:
>>> Animal = Enum('Animal', 'ant bee cat dog')
>>> Animal
<Enum 'Animal'>
>>> Animal.ant
<Animal.ant: 1>
>>> Animal.ant.value
1
>>> list(Animal)
[<Animal.ant: 1>, <Animal.bee: 2>, <Animal.cat: 3>, <Animal.dog: 4>]
이 API의 의미는 namedtuple와 비슷합니다. Enum 호출의 첫 번째 인자는 열거형의 이름입니다. 함수형 API로 만든 열거형의 피클링은 CPython과 PyPy에서 동작하지만, IronPython과 Jython에서는 다음과 같이 모듈 이름을 명시적으로 지정해야 할 수도 있습니다:
>>> Animals = Enum('Animals', 'ant bee cat dog', module=__name__)
두 번째 인자는 열거형 멤버 이름의 출처입니다. 이 인자는 공백으로 구분한 이름 문자열, 이름의 시퀀스, 키/값 쌍으로 이루어진 2-튜플의 시퀀스, 또는 이름을 값에 대응시키는 매핑(예: 딕셔너리)일 수 있습니다. 마지막 두 방식은 열거형에 임의의 값을 할당할 수 있게 해 주며, 나머지 방식은 1부터 시작하여 증가하는 정수를 자동으로 할당합니다. Enum에서 파생된 새 클래스가 반환됩니다. 다시 말해, 위의 Animal에 대한 대입은 다음과 동등합니다:
>>> class Animals(Enum):
... ant = 1
... bee = 2
... cat = 3
... dog = 4
시작 숫자의 기본값이 0이 아니라 1인 이유는 불리언 의미에서 0은 False이지만, 열거형 멤버는 모두 True로 평가되기 때문입니다.
제안된 변형
메일링 리스트에서 논의하는 동안 몇 가지 변형이 제안되었습니다. 다음은 그중 비교적 많은 지지를 받은 것들입니다.
flufl.enum
flufl.enum은 이 PEP가 원래 기반으로 삼았던 참조 구현이었습니다. 결국 flufl.enum은 포함하지 않기로 결정되었습니다. 그 설계가 열거형 멤버를 열거형과 분리하여, 멤버가 열거형의 인스턴스가 아니었기 때문입니다. 또한 그 설계는 더 많은 멤버로 확장하기 위해 열거형을 서브클래싱하는 것을 명시적으로 허용합니다(멤버와 enum이 분리되어 있기 때문에, flufl.enum에서는 이러한 방식으로도 타입 불변 조건이 위반되지 않습니다).
열거형의 값을 지정하지 않아도 되는 방식
Michael Foord는 이를 가능하게 하는 메타클래스 마법을 사용하자고 제안했습니다(Tim Delaney는 개념 증명 구현을 제공했습니다):
class Color(Enum):
red, green, blue
값은 처음 조회될 때에야 실제로 할당됩니다.
장점: 매우 흔한 작업(값은 신경 쓰지 않고 열거형 이름만 나열하는 작업)에서 타이핑을 덜 해도 되는 더 깔끔한 문법입니다.
단점: 구현에 마법이 많이 들어가서, 처음 보는 사람에게는 이러한 열거형의 정의조차 당혹스럽습니다. 게다가 명시적인 것이 암시적인 것보다 낫습니다.
특수한 이름이나 형식을 사용하여 열거형 값을 자동 할당하는 방식
열거형 값을 지정하지 않는 또 다른 방법은 특수한 이름이나 형식을 사용하여 값을 자동으로 할당하는 것입니다. 예를 들면 다음과 같습니다:
class Color(Enum):
red = None # auto-assigned to 0
green = None # auto-assigned to 1
blue = None # auto-assigned to 2
더 유연하게는 다음과 같습니다:
class Color(Enum):
red = 7
green = None # auto-assigned to 8
blue = 19
purple = None # auto-assigned to 20
이 주제에 대한 몇 가지 변형은 다음과 같습니다:
- enum 패키지에서 임포트하는 특수 이름
auto입니다. - Georg Brandl은 같은 효과를 얻기 위해
None대신 생략 기호(...)를 제안했습니다.
장점: 값을 직접 입력할 필요가 없습니다. 특히 큰 열거형에서 열거형을 변경하고 확장하기가 더 쉬워집니다.
단점: 단순한 경우 상당수에서는 오히려 입력할 내용이 더 깁니다. 명시적 대 암묵적이라는 인자가 여기에도 마찬가지로 적용됩니다.
표준 라이브러리에서의 사용 사례
Python 표준 라이브러리에는 enum을 표현하기 위해 현재 사용되는 다른 관용구를 enum 사용으로 대체하면 이점이 있을 곳이 많이 있습니다. 이러한 용도는 사용자 코드에 노출되는 상수와 내부 상수의 두 가지 범주로 나눌 수 있습니다.
os.SEEK_*, socket 모듈 상수, decimal 반올림 모드, HTML 오류 코드처럼 사용자 코드에 노출되는 상수는 사용자 코드가 정수를 기대할 수 있으므로 하위 호환성이 필요할 수 있습니다. 앞서 설명한 IntEnum은 필요한 의미를 제공합니다. int의 서브클래스이므로 정수를 기대하는 사용자 코드에 영향을 주지 않으면서도, 열거형 값을 출력 가능한 형태로 표현할 수 있게 해 줍니다:
>>> import socket
>>> family = socket.AF_INET
>>> family == 2
True
>>> print(family)
SocketFamily.AF_INET
내부 상수는 사용자 코드에 드러나지 않고 표준 라이브러리 모듈 내부에서 사용됩니다. 이러한 상수는 Enum으로 구현할 수 있습니다. 표준 라이브러리를 아주 일부만 훑어보아도 발견되는 예로는 binhex, imaplib, http/client, urllib/robotparser, idlelib, concurrent.futures, turtledemo가 있습니다.
또한 Twisted 라이브러리의 코드를 살펴보면 내부 상태 상수를 열거형으로 대체할 수 있는 사용 사례가 많습니다. 많은 네트워킹 코드(특히 프로토콜 구현)에 대해서도 마찬가지이며, Tulip 라이브러리로 작성된 테스트 프로토콜에서도 이를 확인할 수 있습니다.
감사의 글
이 PEP는 처음에 Barry Warsaw의 flufl.enum 패키지 [8]를 표준 라이브러리에 포함하자는 제안이었으며, 상당 부분 이 패키지에서 영감을 받았습니다. Ben Finney는 이전의 열거형 제안인 PEP 354의 저자입니다.
참고 문헌
Copyright
This document has been placed in the public domain.