PEP 3103 – Switch/Case 문
- Author:
- Guido van Rossum <guido at python.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 25-Jun-2006
- Python-Version:
- 3.0
- Post-History:
- 26-Jun-2006
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
PyCon 2007 기조연설 중에 즉석으로 투표를 해 본 결과, 이 제안은 대중의 지지를 얻지 못했습니다. 따라서 이 제안을 거부합니다.
초록
최근 Python-dev에서 switch 문 추가에 관한 논의가 활발하게 이루어졌습니다. 이 PEP에서 저는 쏟아져 나온 여러 제안 가운데 제가 선호하는 바를 추려 내려 하며, 대안들을 논의하고 가능한 한 제 선택의 이유를 설명합니다. 또한 제가 논의하는 대안들에 대해 제 생각이 얼마나 확고한지도 밝히겠습니다.
이 PEP는 PEP 275의 대안으로 보아야 합니다. 제 견해는 그 PEP 작성자의 견해와 다소 다르지만, 그 PEP에서 이루어진 작업에는 감사하게 생각합니다.
이 PEP는 문법과 의미의 여러 측면에 대해 논의된 수많은 변형, 예를 들어 “alternative 1”, “school II”, “option 3” 등에 대한 정식 이름을 도입합니다. 이 이름들이 논의에 도움이 되기를 바랍니다.
근거
흔히 쓰이는 프로그래밍 관용구 중 하나는 어떤 표현식을 살펴보고 그 값에 따라 서로 다른 일을 하는 것입니다. 이는 보통 if/elif 검사를 연쇄적으로 이어서 처리하며, 저는 이 형태를 “if/elif 체인”이라고 부르겠습니다. 이 관용구를 위한 새로운 문법을 도입하려는 주된 동기는 두 가지입니다:
- 반복적입니다: 변수와 테스트 연산자(보통 ‘==’ 또는 ‘in’)가 각 if/elif 브랜치마다 반복됩니다.
- 비효율적입니다. 표현식이 마지막 검사 값과 일치하면(또는 어떤 검사 값과도 일치하지 않으면), 그 앞의 검사 값 하나하나와 모두 비교됩니다.
두 불만 모두 비교적 가벼운 편입니다. 이를 다르게 작성한다고 해서 가독성이나 성능 면에서 얻을 수 있는 것이 많지는 않습니다. 그렇지만 여러 언어에 어떤 형태로든 switch 문이 있으며, 이를 Python에 추가하면 특정 코드를 이전보다 더 깔끔하고 효율적으로 작성할 수 있으리라고 기대하는 것도 무리는 아닙니다.
제안된 switch 문에 적합하지 않은 디스패치 형태도 있습니다. 예를 들어 case의 개수를 정적으로 알 수 없는 경우나, 각 case의 코드를 서로 다른 클래스나 파일에 두는 것이 바람직한 경우가 그렇습니다.
기본 문법
여기서는 PEP 275에서 처음 제안된 문법의 여러 변형을 검토합니다. 그 밖에도 가능성은 많지만, 그것들이 더해 주는 것은 없다고 봅니다.
저는 최근에 대안 1로 생각을 바꾸었습니다.
여기의 모든 대안에는 “암묵적 break” 속성이 있다는 점을 짚어 두어야 하겠습니다. 즉, 특정 case의 스위트가 끝나면 제어 흐름은 switch 문 전체의 끝으로 이동합니다. 한 case에서 다른 case로 제어를 넘길 방법은 없습니다. 이는 다음 case로 넘어가는 것을 막으려면 명시적인 ‘break’ 문이 필요한 C와 대조적입니다.
모든 대안에서 else 스위트는 선택 사항입니다. 여기서는 C처럼 ‘default’라는 새 예약어를 도입하기보다 ‘else’를 사용하는 편이 더 Python답습니다.
의미론은 다음 최상위 절에서 논의합니다.
대안 1
이것은 PEP 275에서 선호하는 형식입니다:
switch EXPR:
case EXPR:
SUITE
case EXPR:
SUITE
...
else:
SUITE
주된 단점은 실제 동작이 모두 일어나는 스위트가 두 단계 깊이로 들여쓰기된다는 점입니다. 이는 case를 “반 단계”만큼 들여쓰는 방식(예: 일반 들여쓰기 수준이 4칸이라면 2칸)으로 해결할 수 있습니다.
대안 2
이것은 Fredrik Lundh가 선호하는 형식이며, case를 들여쓰지 않는다는 점이 다릅니다:
switch EXPR:
case EXPR:
SUITE
case EXPR:
SUITE
....
else:
SUITE
이 형식을 선택하지 않을 몇 가지 이유로는 자동 들여쓰기 편집기, 폴딩 편집기 등에서 예상되는 어려움과 사용자의 혼란이 있습니다. 현재 Python에는 콜론으로 끝나는 줄 다음에 들여쓰지 않은 줄이 오는 경우가 없습니다.
대안 3
이것은 대안 2와 같지만 switch 뒤의 콜론을 생략합니다:
switch EXPR
case EXPR:
SUITE
case EXPR:
SUITE
....
else:
SUITE
이 대안은 Python을 인식하는 일반적인 텍스트 편집기의 자동 들여쓰기 로직을 덜 혼란스럽게 하리라는 기대에서 나온 것입니다. 하지만 제게는 이상해 보입니다.
대안 4
이것은 ‘case’ 키워드가 중복된다는 이유로 이를 생략합니다:
switch EXPR:
EXPR:
SUITE
EXPR:
SUITE
...
else:
SUITE
안타깝게도 이제는 case 표현식을 들여쓸 수밖에 없습니다. 그렇지 않으면 (적어도 ‘else’ 키워드가 없는 경우) 파서가 들여쓰지 않은 case 표현식(switch 문을 이어가는 것)과 표현식처럼 시작하는 무관한 문(대입이나 프로시저 호출 등)을 구별하기 어렵기 때문입니다. 파서는 콜론을 본 뒤에 역추적할 만큼 똑똑하지 않습니다. 이것은 제가 가장 선호하지 않는 대안입니다.
확장 문법
문법적으로 다루어야 할 추가 문제가 하나 있습니다. 둘 이상의 값을 똑같이 처리해야 하는 경우가 자주 있습니다. C에서는 여러 case 레이블을 그 사이에 아무 코드 없이 나란히 작성하여 이를 처리합니다. 그러면 “fall through” 의미 체계에 따라 이들 모두가 같은 코드로 처리됩니다. Python의 switch는 fall-through 의미 체계를 갖지 않을 것이므로(아직 이를 지지하는 사람이 나타나지 않았습니다) 다른 해결책이 필요합니다. 다음은 몇 가지 대안입니다.
대안 A
다음을 사용하여:
case EXPR:
단일 표현식에 매치하고, 다음을 사용하여:
case EXPR, EXPR, ...:
여러 표현식에 매치합니다. 이 구문은 EXPR이 괄호로 묶인 튜플이거나 값이 튜플인 다른 표현식이면 switch 표현식이 그 튜플의 요소 중 하나가 아니라 그 튜플 자체와 같아야 하는 것으로 해석됩니다. 이는 여러 case를 나타내는 데 변수를 사용할 수 없다는 뜻입니다. 이는 C의 switch 문에서도 마찬가지이지만, Python에서는 비교적 흔히 일어나는 일입니다(예를 들어 sre_compile.py를 참조하십시오).
대안 B
다음을 사용하여:
case EXPR:
단일 표현식에 매치하고, 다음을 사용하여:
case in EXPR_LIST:
여러 표현식에 대해 매칭합니다. EXPR_LIST가 단일 표현식이면, ‘in’은 그것을 이터러블(또는 소수 의견인 대안 의미론에서는 __contains__를 지원하는 무언가)로 해석하도록 강제합니다. 여러 표현식이면, 각 표현식이 매칭 대상으로 고려됩니다.
대안 C
다음을 사용하여:
case EXPR:
단일 표현식에 대해 매칭하고, 다음을 사용하여:
case EXPR, EXPR, ...:
여러 표현식에 대해 매칭하며(대안 A와 같이), 다음을 사용하여:
case *EXPR:
값이 이터러블인 표현식의 요소들에 대해 매칭합니다. 뒤의 두 경우는 결합할 수 있으므로, 실제 문법은 다음에 더 가깝습니다.:
case [*]EXPR, [*]EXPR, ...:
* 표기법은 가변 길이 매개변수 목록과 계산된 인자 목록 전달에 이미 사용되고 있는 접두사 *의 용법과 비슷하며, 값 언패킹에도 자주 제안됩니다(예: (a, b), c = X[:2], X[2:]의 대안으로서의 a, b, *c = X).
대안 D
이것은 대안 B와 C를 혼합한 것입니다. 문법은 대안 B와 비슷하지만 ‘in’ 키워드 대신 ‘*’를 사용합니다. 이 방식은 더 제한적이지만, 여전히 같은 유연성을 제공합니다. 다음을 사용하여:
case EXPR:
단일 표현식에 대해 매칭하고, 다음을 사용하여:
case *EXPR:
이터러블의 요소들에 대해 매칭합니다. 하나의 case에 여러 매칭을 지정하고 싶다면, 다음과 같이 작성할 수 있습니다.:
case *(EXPR, EXPR, ...):
또는 다음과 같이 작성할 수도 있습니다(‘*’와 ‘,’의 상대적 우선순위가 다른 곳과 달라서 조금 이상하기는 합니다).:
case * EXPR, EXPR, ...:
논의
대안 B, C, D는 같은 처리를 받는 여러 case를 일일이 나열하는 대신 집합(보통 튜플)을 나타내는 변수를 사용하여 지정하고자 하는 바람에서 비롯되었습니다. 이에 대한 동기는 보통, 같은 case 집합에 대해 여러 개의 switch가 있을 때 매번 모든 대안을 일일이 나열해야 하는 것은 아쉬운 일이라는 점입니다. 또 다른 동기는 Pascal의 “1..1000:” 표기법과 비슷하게, 매칭할 범위를 쉽고 효율적으로 지정할 수 있도록 하는 것입니다. 동시에 예외 처리에서 흔히 발생하는 종류의 실수(Python 3000에서 except 절의 문법을 변경하여 해결될 예정인 실수)를 방지하고자 합니다. 즉, “case (1, 2):”를 의도했는데 “case 1, 2:”라고 작성하거나, 그 반대로 작성하는 실수입니다.
추가되는 복잡성에 비해 필요성이 충분하지 않다고 주장할 수도 있습니다. C에도 범위를 표현하는 방법이 없으며, 요즘 C는 Pascal보다 훨씬 더 많이 사용됩니다. 또한, 딕셔너리 조회에 기반한 디스패치 방식이 의미론으로 선택된다면, 큰 범위는 비효율적일 수 있습니다(range(1, sys.maxint)를 생각해 보십시오).
종합하면, 제 선호 순서는 (가장 선호하는 것부터 가장 덜 선호하는 것까지) B, A, D’, C이며, 여기서 D’는 세 번째 가능성을 제외한 D입니다.
의미론
올바른 의미론을 선택하기 전에 검토해야 할 몇 가지 쟁점이 있습니다.
If/Elif 체인 대 딕셔너리 기반 디스패치
switch 문의 의미론에 관해서는 몇 가지 주요 학파가 있습니다.
- 학파 I은 switch 문을 동등한 if/elif 체인(경우에 따라 약간의 최적화를 곁들인 것)으로 정의하고자 합니다.
- 학파 II는 이것을 미리 계산된 딕셔너리를 이용한 디스패치로 보는 쪽을 선호합니다. 미리 계산하는 작업을 언제 수행할지에 대해서는 여러 가지 선택지가 있습니다.
- 학파 III도 있습니다. 이 학파는 switch 문을 동등한 if/elif 체인을 기준으로 정의해야 한다는 점에서는 학파 I에 동의합니다. 하지만 관련된 모든 표현식이 해시 가능해야 한다는 점은 최적화 진영에 양보합니다.
학파 I은 다시 학파 Ia와 학파 Ib로 나눌 필요가 있습니다.
- 학파 Ia의 입장은 단순합니다. switch 문은 동등한 if/elif 체인으로 변환되며, 그것이 전부입니다. switch 문을 최적화와 연결해서는 전혀 안 됩니다. 이는 제가 이 학파에 반대하는 주된 이유이기도 합니다. 최적화의 여지가 조금도 없다면 switch 문은 새 문법을 도입할 만큼 매력적이지 않습니다.
- Ib 학파는 더 복잡한 입장을 취합니다. 최적화가 중요하다는 점에서 II 학파에 동의하며, 이를 가능하게 하려고 컴파일러에 어느 정도의 재량을 허용할 의향이 있습니다. (예를 들어 PEP 275 Solution 1.) 특히 switch 표현식과 case 표현식에 대한 해시 함수 hash()는 호출될 수도 있고 호출되지 않을 수도 있습니다(따라서 부작용이 없어야 합니다). 또한 case 표현식은 if/elif 체인 동작에서 기대하는 것처럼 매번 평가되지 않을 수 있으므로, case 표현식 역시 부작용이 없어야 합니다. 이에 대한 제 반론(아래에서 자세히 설명합니다)은, 해시 함수 hash()나 case 표현식 중 어느 하나라도 부작용이 있다면 최적화된 코드와 최적화되지 않은 코드가 다르게 동작할 수 있다는 것입니다.
학파 II는 두 가지 깨달음에서 나왔습니다. 흔히 볼 수 있는 경우를 최적화하는 일이 그리 쉽지 않다는 것, 그리고 이 문제를 정면으로 마주하는 편이 낫다는 것입니다. 이는 아래에서 분명해질 것입니다.
학파 I(주로 학파 Ib)과 학파 II 사이에는 세 가지 차이가 있습니다.
- 디스패치 딕셔너리로 최적화할 때 switch 표현식이나 case 표현식이 해시 불가능할 수 있습니다(이 경우 hash()가 예외를 발생시킵니다). 그러면 학파 Ib는 hash() 실패를 잡아서 if/elif 체인으로 대체할 것을 요구합니다. 학파 II는 그냥 예외가 발생하도록 둡니다. Ib 학파가 요구하는 대로 해시 함수 hash()에서 발생한 예외를 잡을 때의 문제점은, 이것이 진짜 버그를 숨길 수 있다는 것입니다. 한 가지 해결 방법은 모든 case 표현식이 정수, 문자열 또는 해시 동작이 올바르다고 알려진 기타 내장 타입일 때만 디스패치 딕셔너리를 사용하는 것입니다. 그리고 switch 표현식도 그런 타입 중 하나일 때만 해시를 시도합니다. 여기서는 타입 객체도 지원하는 것이 좋을 것입니다. 이것이 학파 III이 다루는 (유일한) 문제입니다.
- 디스패치 dict를 사용하여 최적화할 때, 관련된 어떤 표현식의 해시 함수 hash()가 잘못된 값을 반환하면, Ib 학파에서는 최적화된 코드가 최적화되지 않은 코드와 동일하게 동작하지 않습니다. 이는 최적화 관련 버그에서 잘 알려진 문제이며, 개발자의 시간을 크게 낭비하게 합니다. 학파 II에서는 이런 상황에서도 잘못된 결과가 적어도 일관되게 나오므로 디버깅이 조금 더 쉬워질 것입니다. 앞 항목에서 제안한 해결 방법이 여기서도 도움이 될 것입니다.
- case 표현식이 이름 붙은 상수일 때 학파 Ib에는 쓸 만한 최적화 전략이 없습니다. 컴파일러는 그 값을 확실히 알 수 없으며, 그 값이 정말 상수인지도 알 수 없습니다. 해결 방법으로 다음과 같은 제안이 있었습니다. 딕셔너리가 어느 case를 선택할지 정한 뒤 해당 case의 표현식을 다시 평가합니다. 이렇게 해서 표현식의 값이 바뀌지 않았는지 확인합니다. 하지만 엄밀히 말하면 if/elif 체인의 의미를 제대로 보존하려면 그 case보다 앞에 나오는 case 표현식도 모두 확인해야 합니다. 그러면 최적화의 효과가 완전히 사라집니다. 또 다른 해결책으로 제안된 것은 case 표현식에 관련된 변수나 속성의 값이 변경될 때 이를 디스패치 딕셔너리에 알리는 콜백을 두는 것입니다. 하지만 이는 일반적인 경우에 구현하기 어려워 보입니다. 또한 여러 네임스페이스가 그런 콜백을 지원하는 부담을 져야 하는데, 이런 콜백은 현재 전혀 존재하지 않습니다.
- 끝으로, 중복된 case(즉, 매치 표현식이 같은 값으로 평가되는 case가 둘 이상인 경우)를 어떻게 처리할지에 대해 의견이 다릅니다. 학파 I은 이를 if/elif 체인이 처리하는 것과 똑같이 처리하려 합니다(즉, 첫 번째로 일치한 case가 선택되고, 두 번째로 일치한 case의 코드는 아무 경고 없이 도달할 수 없게 됩니다). 학파 II는 디스패치 딕셔너리가 고정되는 시점에 이것을 오류로 처리하려 합니다(죽은 코드가 진단되지 않은 채 남지 않도록 하기 위해서입니다).
학파 I은 디스패치 딕셔너리를 미리 고정하는 학파 II의 방식에 문제가 있다고 봅니다. 이 방식에서는 프로그래머가 새롭고 낯선 부담을 지게 됩니다. 어떤 종류의 case 값을 고정할 수 있는지, case 값이 언제 고정되는지를 정확히 알아야 합니다. 그렇지 않으면 switch 문의 동작에 놀랄 수 있기 때문입니다.
학파 II는 최적화되지 않은 학파 Ia의 switch가 들이는 노력만큼 가치가 있다고 보지 않습니다. 또한 학파 Ib의 최적화 제안은 최적화된 코드와 최적화되지 않은 코드가 다르게 동작하게 할 수 있으므로 문제가 있다고 봅니다.
또한 II 학파는 해시할 수 없는 값이 포함된 case를 허용하는 데서 별다른 가치를 찾지 못합니다. 사용자가 그런 값을 예상한다면 if/elif 체인을 작성해도 똑같이 쉽게 처리할 수 있기 때문입니다. 또한 II 학파는 딕셔너리 기반 디스패치 구현을 사용하면 이런 문제를 아주 쉽게 잡아낼 수 있는데도, case가 겹쳐서 생기는 죽은 코드를 아무 경고 없이 허용하는 것은 옳지 않다고 생각합니다.
하지만 겹치거나 중복되는 case가 필요한 사용 사례도 있습니다. 운영 체제별 상수(예를 들어 os 모듈이나 그와 비슷한 모듈이 내보내는 상수)를 기준으로 switch를 한다고 가정해 봅시다. 각 상수마다 case가 하나씩 있습니다. 그런데 어떤 운영 체제에서는 서로 다른 두 상수가 같은 값을 가집니다(그 운영 체제에서는 두 상수가 같은 방식으로 구현되기 때문입니다. Unix의 O_TEXT와 O_BINARY가 그런 예입니다). 중복 case를 오류로 처리한다면 그 운영 체제에서는 switch가 아예 동작하지 않을 것입니다. 한 case가 다른 case보다 우선하도록 case들을 배치할 수 있다면 훨씬 나을 것입니다.
(더 흔할 법한) 또 다른 사용 사례로, 똑같이 처리해야 하는 case들의 집합이 있는데 그중 한 원소만 다르게 처리해야 하는 경우도 있습니다. 그 예외를 앞쪽 case에 넣어 두기만 하면 끝나므로 편리할 것입니다.
(물론 실수로 case가 중복되어 생긴 죽은 코드를 진단하지 못하는 것은 아쉬운 일입니다. 어쩌면 그것은 덜 중요한 문제이고, pychecker가 처리할 수 있지 않겠습니까? 어차피 중복된 메서드 정의도 진단하지 않습니다.)
여기서 IIb 학파가 나옵니다. II 학파와 같지만, 중복된 case는 첫 번째로 일치하는 것을 선택하는 방식으로 해결해야 한다는 입장입니다. 이는 디스패치 딕셔너리를 만들 때 아주 간단하게 구현할 수 있습니다(이미 있는 키는 건너뛰면 됩니다).
(다른 방법으로 “case가 겹쳐도 괜찮음”이나 “이 case가 죽은 코드여도 괜찮음”을 나타내는 새 문법을 도입할 수도 있지만, 저는 그것이 지나치다고 생각합니다.)
개인적으로 저는 II 학파입니다. 딕셔너리 기반 디스패치야말로 switch 문의 유일하게 올바른 구현이며, 최대한의 이점을 얻으려면 그 한계를 처음부터 받아들여야 한다고 생각합니다. 저는 IIb 학파 쪽으로 기울고 있습니다. 중복 case는 오류로 처리하는 대신 case의 순서로 해결해야 합니다.
디스패치 딕셔너리를 언제 고정할 것인가
II 학파(딕셔너리 기반 디스패치)를 지지하는 사람들 사이에서 다음으로 크게 갈리는 문제는 switch에 쓸 딕셔너리를 언제 만드느냐입니다. 저는 이것을 “딕셔너리 고정(freezing the dict)”이라고 부릅니다.
이 문제를 흥미롭게 만드는 주된 원인은 Python에 이름 붙은 컴파일 타임 상수가 없다는 점입니다. re.IGNORECASE처럼 개념상 상수인 것도 컴파일러에게는 변수이며, 비뚤어진 코드가 그 값을 바꾸는 것을 막을 방법이 없습니다.
선택지 1
가장 제약이 큰 선택지는 컴파일러에서 딕셔너리를 고정하는 것입니다. 이 방식에서는 case 표현식이 모두 리터럴이거나, 리터럴과 컴파일러가 의미를 알고 있는 연산자만으로 이루어진 컴파일 타임 표현식이어야 합니다. 현재 Python의 동적 의미론과 단일 모듈 컴파일 방식에서는 컴파일러가 그런 표현식에 등장하는 변수의 값을 충분히 확실하게 알 가능성이 전혀 없기 때문입니다. 모두가 그렇게 보는 것은 아니지만, 이 방식은 제약이 지나치게 크다는 평가를 널리 받고 있습니다.
Raymond Hettinger가 이 접근 방식의 주된 지지자입니다. 그는 특정 타입의 리터럴 하나만 case 표현식으로 허용하는 문법을 제안합니다. 이 방식은 모호하지 않고 구현하기 쉽다는 장점이 있습니다.
이에 대한 저의 주된 불만은 “이름 붙은 상수”를 허용하지 않으면 프로그래머가 좋은 습관을 버릴 수밖에 없게 된다는 점입니다. 이름 붙은 상수는 소스 코드에 “매직 넘버”가 등장하는 문제를 해결하기 위해 대부분의 언어에 도입되어 있습니다. 예를 들어, sys.maxint는 2147483647보다 훨씬 읽기 쉽습니다. Raymond는 문자열 리터럴의 내용이 곧 상수에 붙었을 이름이 될 수 있다는 점에 주목하여, 이름 붙은 “열거형” 대신 문자열 리터럴을 사용하자고 제안합니다. 따라서 “case re.IGNORECASE:” 대신 “case ‘IGNORECASE’:”라고 쓸 수 있습니다. 그러나 문자열 리터럴에 철자 오류가 있으면 해당 case는 조용히 무시되며, 버그가 언제 발견될지는 아무도 알 수 없습니다. 반면 NAME에 철자 오류가 있으면 그 오류는 평가되는 즉시 포착됩니다. 또한 상수가 외부에서 정의되는 경우도 있어서(예: JPEG 같은 파일 형식을 파싱할 때) 적절한 문자열 값을 쉽게 고를 수 없습니다. 명시적인 매핑 딕셔너리를 사용하는 것은 조잡한 편법처럼 보입니다.
선택지 2
이 문제를 다루는 가장 오래된 제안은 switch가 처음 실행될 때 디스패치 딕셔너리를 고정하는 것입니다. 이 시점에서는 case 표현식으로 사용된 모든 이름 붙은 “상수”(컴파일러에게는 아니지만 프로그래머의 생각 속에서는 상수인 것)가 정의되어 있다고 가정할 수 있습니다 – 그렇지 않다면 if/elif 체인 역시 성공할 가능성이 거의 없을 것입니다. switch가 여러 번 실행된다고 가정하면, 처음에 약간의 추가 작업을 하더라도 이후의 매우 빠른 디스패치 시간으로 금방 보상받습니다.
이 선택지에 대한 반론 하나는 디스패치 딕셔너리를 저장할 만한 뚜렷한 객체가 없다는 것입니다. 코드 객체는 불변이어야 하므로 코드 객체에 저장할 수 없고, 같은 함수에 대해 여러 함수 객체가 생성될 수 있으므로(예: 중첩 함수의 경우) 함수 객체에도 저장할 수 없습니다. 실제로는 분명 어떤 방법을 찾을 수 있으리라 확신합니다. 두 코드 객체를 비교하거나 코드 객체를 피클링 또는 마셜링할 때 고려되지 않는 코드 객체의 한 영역에 저장할 수도 있고, 코드 객체에 대한 약한 참조를 인덱스로 하는 딕셔너리에 모든 switch를 저장할 수도 있습니다. 또한 해결책은 여러 인터프리터 사이에서 switch 딕셔너리가 새어 나가지 않도록 주의해야 합니다.
또 다른 반론은 최초 사용 규칙이 다음과 같은 난독화된 코드를 허용한다는 것입니다:
def foo(x, y):
switch x:
case y:
print 42
(Python에 익숙하지 않은) 훈련되지 않은 눈에는 이 코드가 다음과 동등해 보일 것입니다:
def foo(x, y):
if x == y:
print 42
하지만 실제로는 그렇게 동작하지 않습니다(항상 두 번째 인자로 같은 값을 넘겨 호출하는 경우가 아니라면 말입니다). 이 문제에 대해서는 case 표현식이 지역 변수를 참조하지 못하도록 해야 한다는 제안이 나왔지만, 이는 다소 자의적입니다.
마지막 반론은 다중 스레드 애플리케이션에서 최초 사용 규칙이 올바른 의미를 보장하려면 복잡한 잠금이 필요하다는 것입니다. (최초 사용 규칙은 case 표현식의 부수 효과가 정확히 한 번만 발생한다는 약속을 암시합니다.) 모든 case 표현식이 평가되는 동안 잠금을 유지해야 하므로, 이는 import 잠금이 그랬던 것만큼이나 까다로울 수 있습니다.
선택지 3
(저를 포함하여) 지지를 얻고 있는 제안은 switch를 포함하는 가장 안쪽 함수가 정의될 때 switch의 딕셔너리를 고정하는 것입니다. switch 딕셔너리는 매개변수 기본값과 마찬가지로 함수 객체에 저장되며, 실제로 case 표현식은 매개변수 기본값과 같은 시점에, 같은 스코프(즉, 함수 정의를 포함하는 스코프)에서 평가됩니다.
이 선택지는 선택지 2를 작동시키는 데 필요한 여러 세심한 처리를 피할 수 있다는 장점이 있습니다. 잠금이 필요 없고, 불변 코드 객체나 여러 인터프리터에 대해 걱정할 필요도 없습니다. 또한 case 표현식에서 지역 변수를 참조할 수 없는 이유도 명확하게 설명해 줍니다.
이 선택지는 일반적으로 switch를 사용할 만한 상황에서도 똑같이 잘 작동합니다. 임포트되었거나 전역으로 정의된 이름 붙은 상수를 포함하는 case 표현식은, 함수 정의를 만나기 전에 임포트되거나 정의되어 있기만 하면 선택지 2와 정확히 같은 방식으로 작동합니다.
그러나 단점은 중첩 함수 안에 있는 switch의 디스패치 딕셔너리를 중첩 함수가 정의될 때마다 다시 계산해야 한다는 것입니다. 특정한 “함수형” 프로그래밍 스타일에서는 이 때문에 중첩 함수 안에서 switch를 쓰는 것이 매력적이지 않을 수 있습니다. (모든 case 표현식이 컴파일 타임 상수인 경우는 예외입니다. 그 경우 컴파일러는 당연히 switch 고정 코드를 최적화하여 제거하고 디스패치 테이블을 코드 객체의 일부로 만들 수 있습니다.)
또 다른 단점은 이 선택지에서는 함수 안에 있지 않은 switch의 디스패치 딕셔너리가 고정되는 명확한 시점이 없다는 것입니다. 함수 밖의 switch를 어떻게 다룰지에 대해서는 몇 가지 실용적인 선택지가 있습니다:
- 금지합니다.
- if/elif 연쇄로 변환합니다.
- 컴파일 시점 상수 표현식만 허용합니다.
- switch에 도달할 때마다 디스패치 딕셔너리를 계산합니다.
- (b)와 같지만, 평가되는 모든 표현식이 해시 가능한지 검사합니다.
이 중에서 (a)는 지나치게 제한적으로 보입니다. (c)보다 모든 면에서 못하기 때문입니다. 그리고 (d)는 (b)에 비해 이점이 거의 또는 전혀 없으면서 성능이 나쁩니다. 모듈 수준에서는 모든 지역 변수 참조가 느리므로, 그곳에 성능이 중요한 내부 루프를 두는 것은 말이 되지 않습니다. 따라서 저는 (b)를 (약하게) 선호합니다. 어쩌면 switch의 비전형적인 사용을 막으려 하는 (e)를 선호해야 할지도 모릅니다. 대화형으로는 동작하지만 함수 안에서는 동작하지 않는 예제는 성가시기 때문입니다. 결국 저는 이 문제가 그다지 중요하다고 생각하지 않으며(어떻게든 해결되어야 한다는 점만 빼면), 최종적으로 이를 구현하게 될 사람에게 맡길 의향이 있습니다.
switch가 함수 안이 아니라 클래스 안에 나타나는 경우, 클래스 본문을 나타내는 임시 함수 객체가 생성되는 시점에 디스패치 딕셔너리를 고정할 수 있습니다. 이는 case 표현식이 모듈 전역 변수는 참조할 수 있지만 클래스 변수는 참조할 수 없다는 뜻입니다. 또는 위에서 (b)를 선택한다면, 클래스 정의 안에서도 이 구현을 선택할 수 있습니다.
옵션 4
함수 정의 시점에 미리 계산된 값이라는 개념을 매개변수 기본값이나 case 표현식에 묶지 않고 일반적으로 사용할 수 있게 하는 구문을 언어에 추가하자는 제안이 여럿 있습니다. 제안된 키워드로는 ‘const’, ‘static’, ‘only’, ‘cached’ 등이 있습니다. 관련 문법과 의미는 제안마다 다릅니다.
이러한 제안은 이 PEP의 범위를 벗어납니다. 다만 만약 그러한 제안이 받아들여진다면 switch가 이득을 볼 수 있는 방법이 두 가지 있다는 점만 언급해 둡니다. case 표현식이 컴파일 시점 상수이거나 미리 계산된 값이도록 요구할 수도 있고, 미리 계산된 값을 case 표현식의 기본(이자 유일한) 평가 방식으로 만들 수도 있습니다. 저는 후자를 선호합니다. 명시적인 if/elif 연쇄를 작성하는 것으로 충분히 해결되지 않는, 더 동적인 case 표현식의 용도를 찾을 수 없기 때문입니다.
결론
결정하기에는 아직 너무 이릅니다. 결정하기 전에 미리 계산된 값에 대한 완성된 제안을 적어도 하나는 보고 싶습니다. 그동안 Python은 switch 문 없이도 괜찮으며, 어쩌면 switch 문을 추가하는 것이 실수라고 주장하는 사람들이 옳을지도 모릅니다.
Copyright
This document has been placed in the public domain.