PEP 3103 – 스위치/케이스 문
- 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 chain”이라고 부르겠습니다. 이 관용구에 새로운 구문을 도입하려는 주된 이유는 두 가지입니다.
- 반복적입니다. 변수와 테스트 연산자(보통 ‘==’ 또는 ‘in’)가 각 if/elif 분기에서 반복됩니다.
- 비효율적입니다. 표현식이 마지막 테스트 값과 일치할 때(또는 테스트 값과 전혀 일치하지 않을 때), 앞선 각 테스트 값과 비교됩니다.
이 두 불만은 모두 비교적 사소합니다. 이를 다르게 작성해도 가독성이나 성능 면에서 얻을 수 있는 이점은 크지 않습니다. 그럼에도 여러 언어에서 어떤 형태로든 switch 문을 찾아볼 수 있으며, 이를 Python에 추가하면 특정 코드를 이전보다 더 깔끔하고 효율적으로 작성할 수 있으리라고 기대하는 것은 무리가 아닙니다.
제안된 switch 문에 적합하지 않은 디스패치 형태도 있습니다. 예를 들어 케이스 수를 정적으로 알 수 없는 경우나, 서로 다른 케이스의 코드를 서로 다른 클래스 또는 파일에 배치하는 것이 바람직한 경우입니다.
기본 구문
여기서는 PEP 275에서 처음 제안된 구문의 여러 변형을 검토하고 있습니다. 다른 가능성도 많이 있지만, 그것들이 무엇인가를 더해 준다고는 생각하지 않습니다.
최근에는 대안 1로 생각이 바뀌었습니다.
여기서 모든 대안에는 “implicit break” 속성이 있다는 점을 언급해야 합니다. 특정 케이스의 suite가 끝나면 제어 흐름이 전체 switch 문의 끝으로 이동합니다. 한 케이스에서 다른 케이스로 제어를 넘길 방법은 없습니다. 이는 다음 케이스로 폴스루하는 것을 막으려면 명시적인 ‘break’ 문이 필요한 C와는 대조적입니다.
모든 대안에서 else-suite는 선택 사항입니다. C에서처럼 새로운 예약어인 ‘default’를 도입하기보다는 여기에서 ‘else’를 사용하는 것이 더 Python답습니다.
의미론은 다음 최상위 섹션에서 논의합니다.
대안 1
이것은 PEP 275에서 선호하는 형식입니다.:
switch EXPR:
case EXPR:
SUITE
case EXPR:
SUITE
...
else:
SUITE
가장 큰 단점은 모든 동작이 이루어지는 스위트가 두 단계 깊게 들여쓰기된다는 점입니다. 일반적인 들여쓰기 수준이 4칸인 경우 2칸처럼 케이스를 “반 단계” 들여쓰기하면 이를 해결할 수 있습니다.
대안 2
이것은 Fredrik Lundh가 선호하는 형식이며, 케이스를 들여쓰기하지 않는다는 점이 다릅니다.:
switch EXPR:
case EXPR:
SUITE
case EXPR:
SUITE
....
else:
SUITE
이를 선택하지 않을 몇 가지 이유로는 자동 들여쓰기 편집기, 접기 편집기 등의 예상되는 어려움과 혼란스러워하는 사용자가 있습니다. 현재 Python에는 콜론으로 끝나는 줄 뒤에 들여쓰기되지 않은 줄이 오는 상황이 없습니다.
대안 3
이것은 대안 2와 같지만 스위치 뒤의 콜론을 생략합니다.:
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이 괄호로 묶인 튜플이거나 값이 튜플인 다른 표현식인 경우, 스위치 표현식이 그 요소 중 하나가 아니라 해당 튜플과 같아야 하는 것으로 해석됩니다. 이는 여러 케이스를 나타내기 위해 변수를 사용할 수 없다는 의미입니다. 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는 (a, b), c = X[:2], X[2:]의 대안입니다).
대안 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에도 범위를 표현하는 방법이 없으며, 오늘날에는 Pascal보다 훨씬 더 많이 사용됩니다. 또한 dict 조회를 기반으로 하는 디스패치 방식이 의미 체계로 선택된다면, 큰 범위는 비효율적일 수 있습니다(range(1, sys.maxint)를 고려하십시오).
종합하면 제가 선호하는 순서는(선호도가 높은 순서부터) B, A, D’, C이며, 여기서 D’는 세 번째 가능성을 제외한 D입니다.
의미론
올바른 의미론을 선택하기 전에 검토해야 할 몇 가지 쟁점이 있습니다.
If/Elif 체인 대 딕셔너리 기반 디스패치
switch 문의 의미론에 대해서는 몇 가지 주요 학파가 있습니다.
- 학파 I은 switch 문을 동등한 if/elif 체인(일부 최적화를 적용할 수도 있음)의 관점에서 정의하려고 합니다.
- 학파 II는 이를 사전 계산된 디스패치 딕셔너리에 의한 디스패치로 보는 것을 선호합니다. 사전 계산이 언제 이루어지는지에 대해서는 여러 선택지가 있습니다.
- 학파 III도 있습니다. 학파 I와 마찬가지로 switch 문의 정의는 동등한 if/elif 체인의 관점에서 이루어져야 한다고 보지만, 관련된 모든 표현식이 해시 가능해야 한다는 점에서는 최적화 진영의 입장을 받아들입니다.
학파 I를 학파 Ia와 학파 Ib로 더 나누어야 합니다.
- 학파 Ia의 입장은 간단합니다. switch 문은 동등한 if/elif 체인으로 변환되며, 그게 전부입니다. switch 문은 최적화와 전혀 연관되어서는 안 됩니다. 이것이 제가 이 학파에 제기하는 주된 반론이기도 합니다. 최적화의 기미가 전혀 없다면 switch 문은 새로운 문법을 도입할 만큼 매력적이지 않습니다.
- 학파 Ib의 입장은 더 복잡합니다. 최적화가 중요하다는 점에서는 학파 II에 동의하며, 이를 가능하게 하기 위해 컴파일러에 일정한 재량을 허용할 의향이 있습니다. (예를 들어, PEP 275 Solution 1이 있습니다.) 특히 switch 표현식과 case 표현식에 대해 hash()가 호출될 수도 있고 호출되지 않을 수도 있으므로 부작용이 없어야 합니다. 또한 if/elif 체인의 동작에서 예상되는 것처럼 case 표현식이 매번 평가되지 않을 수도 있으므로 case 표현식에도 부작용이 없어야 합니다. 이에 대한 제 반론(아래에서 자세히 설명합니다)은 hash() 또는 case 표현식 중 하나라도 부작용이 있다면 최적화된 코드와 최적화되지 않은 코드가 다르게 동작할 수 있다는 점입니다.
학파 II는 흔히 발생하는 경우의 최적화가 그다지 쉽지 않으며, 이 문제를 정면으로 다루는 편이 낫다는 인식에서 생겨났습니다. 이는 아래에서 명확해질 것입니다.
학파 I(대부분 학파 Ib)와 학파 II의 차이는 세 가지입니다.
- 디스패치 딕셔너리를 사용하여 최적화할 때 switch 표현식이나 case 표현식 중 하나라도 해시 불가능하면(hash()가 예외를 발생시키는 경우), 학파 Ib에서는 hash() 실패를 포착하고 if/elif 체인으로 대체해야 합니다. 학파 II에서는 단순히 예외가 발생하도록 둡니다. 학파 Ib에서 요구하는 것처럼 hash()에서 발생한 예외를 포착할 때의 문제는 이것이 실제 버그를 숨길 수 있다는 점입니다. 가능한 해결책은 모든 case 표현식이 정수, 문자열 또는 해시 동작이 확실히 알려진 다른 내장 타입일 때만 디스패치 딕셔너리를 사용하고, switch 표현식도 그러한 타입 중 하나일 때만 해시를 시도하는 것입니다. 여기에는 타입 객체도 지원하는 것이 좋을 것입니다. 이것이 학파 III가 다루는 유일한 문제입니다.
- 디스패치 딕셔너리를 사용하여 최적화할 때 관련된 표현식 중 하나라도 hash() 함수에서 잘못된 값을 반환하면, 학파 Ib에서는 최적화된 코드가 최적화되지 않은 코드와 동일하게 동작하지 않습니다. 이는 최적화 관련 버그에서 잘 알려진 문제이며 개발자의 시간을 많이 낭비하게 합니다. 학파 II에서는 이 상황에서 적어도 일관되게 잘못된 결과가 생성되므로 디버깅이 조금 더 쉬워질 것입니다. 앞 항목에서 제안한 해결책은 이 문제에도 도움이 될 것입니다.
- case 표현식이 명명된 상수인 경우 학파 Ib에는 좋은 최적화 전략이 없습니다. 컴파일러는 해당 값이 무엇인지 확실히 알 수 없으며, 실제로 상수인지 여부도 알 수 없습니다. 해결책으로 디스패치 딕셔너리가 어떤 case를 선택해야 하는지 식별한 후 해당 case에 대응하는 표현식을 다시 평가하여 표현식의 값이 변경되지 않았는지 확인하자는 제안이 있었습니다. 그러나 엄밀히 말하면 진정한 if/elif 체인의 의미를 보존하려면 해당 case 앞에 나오는 모든 case 표현식도 확인해야 하므로, 결국 최적화가 완전히 무의미해집니다. 또 다른 제안된 해결책은 case 표현식에 관련된 변수나 속성의 값이 변경될 때 디스패치 딕셔너리에 알리는 콜백을 두는 것입니다. 그러나 이는 일반적인 경우에 구현하기 어려울 가능성이 높으며, 현재는 그러한 콜백이 전혀 존재하지 않는 많은 네임스페이스가 이를 지원하는 부담을 져야 합니다.
- 마지막으로 중복 case의 처리에 대해서는 의견 차이가 있습니다(즉, 동일한 값으로 평가되는 match 표현식을 가진 case가 두 개 이상인 경우입니다). 학파 I는 이를 if/elif 체인이 처리하는 방식과 동일하게 처리하기를 원합니다(즉, 첫 번째 일치가 우선하며 두 번째 일치에 해당하는 코드는 조용히 도달할 수 없게 됩니다). 학파 II는 디스패치 딕셔너리가 고정되는 시점에 이를 오류로 처리하기를 원합니다(따라서 죽은 코드가 진단 없이 넘어가지 않습니다).
학파 I는 학파 II의 디스패치 딕셔너리 사전 고정 방식에 문제가 있다고 봅니다. 이 방식은 프로그래머에게 어떤 종류의 case 값이 고정될 수 있는지와 case 값이 언제 고정되는지를 정확히 이해해야 하는 새롭고 이례적인 부담을 지우며, 그렇지 않으면 switch 문의 동작에 당황할 수 있기 때문입니다.
학파 II는 학파 Ia의 최적화되지 않은 switch가 그 수고를 들일 가치가 있다고 생각하지 않으며, 최적화된 코드와 최적화되지 않은 코드가 다르게 동작하게 만들 수 있는 학파 Ib의 최적화 제안에도 문제가 있다고 봅니다.
또한 II 학파는 해시 불가능한 값과 관련된 케이스를 허용하는 것이 별로 가치가 없다고 봅니다. 결국 사용자가 그러한 값을 예상한다면, if/elif 연쇄를 작성하는 것만큼이나 쉽게 처리할 수 있기 때문입니다. 또한 딕셔너리 기반 디스패치 구현에서는 이러한 문제를 쉽게 포착할 수 있는데도, 겹치는 케이스로 인해 발생하는 도달 불가능 코드를 아무런 표시 없이 허용하는 것이 옳다고 II 학파는 생각하지 않습니다.
그러나 겹치거나 중복되는 케이스에도 몇 가지 사용 사례가 있습니다. 일부 운영 체제별 상수를 기준으로 전환한다고 가정하십시오(예를 들어 os 모듈이나 이와 유사한 모듈이 내보내는 상수입니다). 각 상수에 해당하는 케이스가 있습니다. 그러나 일부 운영 체제에서는 서로 다른 두 상수가 같은 값을 가집니다(해당 운영 체제에서는 같은 방식으로 구현되기 때문이며, Unix의 O_TEXT와 O_BINARY가 그 예입니다). 중복 케이스가 오류로 표시된다면 해당 운영 체제에서는 switch가 전혀 작동하지 않을 것입니다. 한 케이스가 다른 케이스보다 우선하도록 케이스를 배치할 수 있다면 훨씬 좋을 것입니다.
또한 여러 케이스를 동일하게 처리해야 하지만 그 집합의 한 구성원은 다르게 처리해야 하는, 더 가능성이 높은 사용 사례도 있습니다. 예외를 더 앞선 케이스에 넣고 끝낼 수 있다면 편리할 것입니다.
(그렇습니다. 실수로 케이스가 중복되어 발생하는 도달 불가능 코드를 진단할 수 없다는 것은 안타까운 일처럼 보입니다. 어쩌면 그것은 덜 중요하고, pychecker가 처리할 수 있을지도 모릅니다. 결국 중복된 메서드 정의도 진단하지 않기 때문입니다.)
이는 IIb 학파를 시사합니다. II 학파와 같지만, 불필요한 케이스는 첫 번째 일치를 선택하여 해결해야 합니다. 디스패치 딕셔너리를 만들 때 이미 존재하는 키를 건너뛰면 되므로 구현은 간단합니다.
(또 다른 방법은 “겹치는 케이스를 허용해도 됨” 또는 “이 케이스가 도달 불가능 코드여도 됨”을 나타내는 새로운 구문을 도입하는 것이겠지만, 저는 그것이 과도하다고 생각합니다.)
개인적으로 저는 II 학파에 속합니다. 딕셔너리 기반 디스패치가 switch 문을 위한 유일하게 올바른 구현이며, 최대한의 이점을 얻을 수 있도록 한계를 처음부터 받아들여야 한다고 생각합니다. 저는 IIb 학파 쪽으로 기울고 있습니다. 중복 케이스는 오류로 표시하는 대신 케이스의 순서에 따라 해결해야 합니다.
디스패치 딕셔너리를 언제 동결할 것인가
II 학파(딕셔너리 기반 디스패치)의 지지자들에게 다음으로 중요한 쟁점은 전환에 사용할 딕셔너리를 언제 생성할 것인가입니다. 저는 이를 “딕셔너리 동결”이라고 부릅니다.
이 문제를 흥미롭게 만드는 주요 원인은 Python에 이름이 붙은 컴파일 시간 상수가 없다는 점입니다. 개념적으로 상수인 re.IGNORECASE 같은 것도 컴파일러에게는 변수이며, 악의적인 코드가 그 값을 수정하지 못하도록 막을 방법이 없습니다.
선택지 1
가장 제한적인 선택지는 컴파일러에서 딕셔너리를 동결하는 것입니다. 이를 위해서는 케이스 표현식이 모두 리터럴이거나, 리터럴과 의미가 컴파일러에 알려진 연산자만을 포함하는 컴파일 시간 표현식이어야 합니다. 현재 Python의 동적 의미론과 단일 모듈 컴파일 상태에서는 그러한 표현식에 포함된 변수의 값을 컴파일러가 충분한 확실성을 가지고 알아낼 가능성이 없기 때문입니다. 이는 널리 그렇게 여겨지지만, 모든 사람이 지나치게 제한적이라고 생각하는 것은 아닙니다.
Raymond Hettinger가 이 접근 방식의 주요 지지자입니다. 그는 케이스 표현식으로 특정 유형의 단일 리터럴만 허용하는 구문을 제안합니다. 모호하지 않고 구현하기 쉽다는 장점이 있습니다.
이에 대한 저의 주된 불만은 “명명된 상수”를 허용하지 않음으로써 프로그래머에게 좋은 습관을 포기하도록 강요한다는 것입니다. 명명된 상수는 소스 코드에 나타나는 “매직 넘버” 문제를 해결하기 위해 대부분의 언어에서 도입되었습니다. 예를 들어, sys.maxint는 2147483647보다 훨씬 읽기 쉽습니다. Raymond는 명명된 “열거형” 대신 문자열 리터럴을 사용할 것을 제안하면서, 문자열 리터럴의 내용이 상수가 그렇지 않으면 가지게 될 이름이 될 수 있다고 설명합니다. 따라서 “case ‘IGNORECASE’:”를 “case re.IGNORECASE:” 대신 작성할 수 있습니다. 그러나 문자열 리터럴에 철자 오류가 있으면 해당 case가 조용히 무시되며, 버그가 언제 발견될지는 아무도 모릅니다. 반면 NAME에 철자 오류가 있으면 평가되는 즉시 오류가 포착됩니다. 또한 상수가 외부에서 정의되는 경우도 있으며(예: JPEG와 같은 파일 형식을 구문 분석할 때), 적절한 문자열 값을 쉽게 선택할 수 없습니다. 명시적인 매핑 딕셔너리를 사용하는 것은 서투른 임시방편처럼 보입니다.
옵션 2
이를 처리하기 위한 가장 오래된 제안은 스위치가 처음 실행될 때 디스패치 딕셔너리를 고정하는 것입니다. 이 시점에서는 case 표현식으로 사용되는 모든 명명된 “상수”(컴파일러에게는 상수가 아니지만 프로그래머의 관점에서는 상수)가 정의되어 있다고 가정할 수 있습니다. 그렇지 않으면 if/elif 체인 역시 성공할 가능성이 거의 없기 때문입니다. 스위치가 여러 번 실행된다고 가정하면, 처음에 약간의 추가 작업을 수행하는 것이 이후 매우 빠른 디스패치 시간으로 빠르게 보상됩니다.
이 옵션에 대한 반론은 디스패치 딕셔너리를 저장할 명확한 객체가 없다는 것입니다. 변경 불가능해야 하는 코드 객체에 저장할 수 없으며, 동일한 함수에 대해 여러 함수 객체가 생성될 수 있으므로(예: 중첩 함수의 경우) 함수 객체에도 저장할 수 없습니다. 실제로는 방법을 찾을 수 있으리라고 확신합니다. 두 코드 객체를 비교하거나 코드 객체를 피클링 또는 마샬링할 때 고려하지 않는 코드 객체의 한 영역에 저장할 수도 있고, 약한 참조로 코드 객체를 인덱싱하는 딕셔너리에 모든 스위치를 저장할 수도 있습니다. 또한 이 해결책은 여러 인터프리터 사이에서 스위치 딕셔너리가 유출되지 않도록 주의해야 합니다.
또 다른 반론은 최초 사용 규칙이 다음과 같은 난독화된 코드를 허용한다는 것입니다.:
def foo(x, y):
switch x:
case y:
print 42
파이썬에 익숙하지 않은 훈련받지 않은 눈에는 이 코드가 다음과 동등해 보일 것입니다.:
def foo(x, y):
if x == y:
print 42
그러나 실제로는 그렇게 동작하지 않습니다(두 번째 인자와 같은 값으로만 항상 호출되는 경우가 아니라면 그렇습니다). 이에 대한 대응으로 case 표현식에서 지역 변수를 참조할 수 없도록 해야 한다는 제안이 나왔지만, 이는 다소 자의적입니다.
마지막 반론은 다중 스레드 애플리케이션에서 정확한 의미론을 보장하려면 최초 사용 규칙에 복잡한 잠금이 필요하다는 것입니다. (최초 사용 규칙은 case 표현식의 부작용이 정확히 한 번만 발생한다는 보장을 시사합니다.) 모든 case 표현식을 평가하는 동안 잠금을 유지해야 하므로, 이는 임포트 잠금이 까다로운 것으로 입증된 것만큼이나 까다로울 수 있습니다.
옵션 3
지지를 얻고 있는 제안(저도 지지합니다)은 스위치를 포함하는 가장 안쪽 함수가 정의될 때 스위치의 딕셔너리를 고정하는 것입니다. 스위치 딕셔너리는 매개변수 기본값과 마찬가지로 함수 객체에 저장되며, 실제로 case 표현식은 매개변수 기본값과 동일한 시점에 동일한 스코프에서 평가됩니다(즉, 함수 정의를 포함하는 스코프에서 평가됩니다).
이 옵션은 옵션 2를 작동시키는 데 필요한 여러 기교를 피할 수 있다는 장점이 있습니다. 잠금이 필요하지 않고, 변경 불가능한 코드 객체나 여러 인터프리터에 대해 걱정할 필요도 없습니다. 또한 case 표현식에서 지역 변수를 참조할 수 없는 이유를 명확하게 설명해 줍니다.
이 옵션은 일반적으로 스위치를 사용할 상황에서도 똑같이 잘 작동합니다. 가져온 명명된 상수나 전역 명명된 상수를 포함하는 case 표현식은 함수 정의에 도달하기 전에 해당 상수가 가져와졌거나 정의되어 있기만 하면 옵션 2와 정확히 같은 방식으로 작동합니다.
그러나 단점은 중첩 함수 내부의 스위치에 대한 디스패치 딕셔너리를 중첩 함수가 정의될 때마다 다시 계산해야 한다는 것입니다. 특정 “함수형” 프로그래밍 스타일에서는 이 때문에 중첩 함수에서 스위치가 매력적이지 않을 수 있습니다. (모든 case 표현식이 컴파일 타임 상수인 경우는 예외입니다. 이때 컴파일러는 당연히 스위치 고정 코드를 최적화하여 제거하고 디스패치 테이블을 코드 객체의 일부로 만들 수 있습니다.)
그러나 이 옵션의 또 다른 단점은 함수 내부에 발생하지 않는 스위치의 경우 디스패치 딕셔너리가 고정되는 명확한 시점이 없다는 것입니다. 함수 외부의 스위치를 처리하는 방법에는 몇 가지 실용적인 선택지가 있습니다.
- 이를 허용하지 마십시오.
- 이를 if/elif 체인으로 변환합니다.
- 컴파일 타임 상수 표현식만 허용합니다.
- switch에 도달할 때마다 디스패치 딕셔너리를 계산합니다.
- (b)와 같지만 평가되는 모든 표현식이 해시 가능한지 테스트합니다.
이 중 (a)는 지나치게 제한적인 것 같습니다. (c)보다 모든 면에서 나쁘기 때문입니다. 또한 (d)는 (b)에 비해 이점이 거의 또는 전혀 없으면서 성능이 좋지 않습니다. 모든 지역 변수 참조가 그곳에서는 느리므로 모듈 수준에 성능이 중요한 내부 루프를 두는 것은 의미가 없습니다. 따라서 (b)가 제가 (약하게) 선호하는 선택입니다. 어쩌면 비정상적인 switch 사용을 방지하려는 (e)를 선호해야 할지도 모르겠습니다. 대화형으로는 작동하지만 함수에서는 작동하지 않는 예제는 성가십니다. 결국 이 문제가 그렇게 중요하다고 생각하지는 않으며(어떤 식으로든 해결해야 한다는 점은 제외하고), 최종적으로 이를 구현하게 될 사람에게 맡겨도 좋다고 생각합니다.
switch가 함수가 아닌 클래스에서 나타나는 경우, 클래스 본문을 나타내는 임시 함수 객체가 생성되는 동시에 디스패치 딕셔너리를 고정할 수 있습니다. 이는 케이스 표현식이 모듈 전역 변수는 참조할 수 있지만 클래스 변수는 참조할 수 없음을 의미합니다. 또는 위의 (b)를 선택한다면, 클래스 정의 내부에서도 이 구현을 선택할 수 있습니다.
선택지 4
매개변수 기본값이나 케이스 표현식에 한정하지 않고, 함수 정의 시점에 미리 계산되는 값이라는 개념을 일반적으로 사용할 수 있도록 하는 구문을 언어에 추가하자는 제안이 여러 가지 있습니다. 제안된 키워드로는 ‘const’, ‘static’, ‘only’ 또는 ‘cached’ 등이 있습니다. 관련 구문과 의미론은 서로 다릅니다.
이러한 제안은 이 PEP의 범위를 벗어납니다. 다만 if 그러한 제안이 받아들여진다면 switch가 이점을 얻을 수 있는 방법은 두 가지라고 제안할 수 있습니다. 케이스 표현식이 컴파일 타임 상수이거나 미리 계산된 값 중 하나이도록 요구하거나, 미리 계산된 값을 케이스 표현식의 기본이자 유일한 평가 모드로 만들 수 있습니다. 후자를 선호합니다. 명시적인 if/elif 체인을 작성하면 충분히 처리되지 않는 더 동적인 케이스 표현식의 용도를 찾지 못했기 때문입니다.
결론
결정하기에는 아직 너무 이릅니다. 결정하기 전에 미리 계산된 값에 대한 완성된 제안을 적어도 하나는 보고 싶습니다. 그동안 Python은 switch 문 없이도 잘 작동하며, switch 문을 추가하는 것은 실수라고 주장하는 사람들이 옳을지도 모릅니다.
Copyright
This document has been placed in the public domain.