Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 275 – 여러 값에 대한 스위칭

Author:
Marc-André Lemburg <mal at lemburg.com>
Status:
Rejected
Type:
Standards Track
Created:
10-Nov-2001
Python-Version:
2.6
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부 공지

Python 3000을 위한 유사한 PEP인 PEP 3103이 이미 거부되었으므로, 이 제안 역시 채택될 가능성이 없습니다.

초록

이 PEP는 여러 가능한 값 중 하나를 갖는 단일 변수에 대한 스위칭을 처리할 때 Python의 성능을 향상하기 위한 전략을 제안합니다.

문제

Python 2.5까지 다중 값 스위치를 작성하는 전형적인 방법은 다음과 같은 형태의 긴 스위치 구문을 사용하는 것이었습니다.:

if x == 'first state':
    ...
elif x == 'second state':
    ...
elif x == 'third state':
    ...
elif x == 'fourth state':
    ...
else:
    # default handling
    ...

짧은 스위치 구문에서는 이 방식이 잘 동작합니다. 지역 변수(이 경우 변수 x)를 반복해서 로드하고 이를 어떤 상수와 비교하는 오버헤드가 낮기 때문입니다(평균적으로 O(n)의 복잡도를 가집니다). 그러나 파서 작성에 필요한 것과 같은 상태 기계를 이러한 구문으로 작성하면, 가능한 상태의 수가 쉽게 10개 이상에 이를 수 있습니다.

이 문제에 대한 현재의 해결책은 디스패치 테이블을 사용하여 스위치 변수의 값에 따라 실행할 케이스 구현 메서드를 찾는 것입니다(예를 들어 완전 해시 테이블을 사용하면 평균적으로 O(1)의 복잡도를 갖도록 조정할 수 있습니다). 이 방식은 각 케이스 메서드에서 복잡하고 긴 처리가 필요한 상태 기계에는 잘 동작합니다. 하지만 케이스마다 한두 개의 명령만 처리하는 상태 기계에서는 성능이 좋지 않습니다. 예를 들면 다음과 같습니다.

def handle_data(self, data):
    self.stack.append(data)

이에 대한 좋은 예로는 Python 객체를 직렬화하는 데 사용되는 pickle.py에 구현된 상태 기계가 있습니다. 그 밖의 대표적인 사례로는 XML SAX 파서와 인터넷 프로토콜 핸들러가 있습니다.

제안하는 해결책

이 PEP는 서로 다르지만 반드시 상충하지는 않는 두 가지 해결책을 제안합니다.

  1. 위의 if-elif-else 구문을 감지하고, 점프 오프셋을 저장하기 위해 읽기 전용 딕셔너리를 사용하는 특수 옵코드를 생성하는 최적화를 Python 컴파일러와 VM에 추가합니다.
  2. C 스타일의 switch 문을 모방하는 새로운 문법을 Python에 추가합니다.

첫 번째 해결책은 언어에 새 키워드를 추가하지 않아도 된다는 장점이 있고, 두 번째 해결책은 더 깔끔해 보입니다. 두 해결책 모두 스위칭 변수가 불변이며 해시 가능함을 보장하기 위한 약간의 런타임 오버헤드를 수반합니다.

두 해결책 모두 올바른 점프 위치를 찾기 위해 딕셔너리 조회를 사용하므로, 스위치 변수와 상수 모두가 딕셔너리 구현과 호환되어야 한다는(해시 가능, 비교 가능, a==b => hash(a)==hash(b)) 점에서 동일한 문제 영역을 공유합니다.

해결책 1: if-elif-else 최적화

구현:

컴파일러는 다음과 같은 시그니처를 갖는 if-elif-else 구문을 감지할 수 있어야 합니다.:

if x == 'first':...
elif x == 'second':...
else:...

즉, 좌변은 항상 같은 변수를 참조하고, 우변은 해시 가능한 불변 내장 타입입니다. 우변이 모두 같은 타입일 필요는 없지만, 좌변의 스위치 변수의 타입과 비교 가능해야 합니다.

그런 다음 컴파일러는 읽기 전용 (완전) 해시 테이블을 구성하여 상수에 저장하고, 표준 if-elif-else 바이트코드 스트림 앞에 SWITCH 옵코드를 추가할 수 있습니다. 이 옵코드는 다음과 같은 런타임 동작을 일으킵니다.

런타임에 SWITCH는 x가 잘 알려진 불변 타입(문자열, 유니코드, 숫자) 중 하나인지 검사하고, 해시 테이블을 사용하여 올바른 옵코드 조각을 찾습니다. 이 조건이 충족되지 않으면, 인터프리터는 단순히 SWITCH 옵코드를 건너뛰고 일반적인 if-elif-else 바이트코드 스트림을 계속 진행함으로써 표준 if-elif-else 처리로 되돌아가야 합니다.

문제점:

새로운 최적화는 (최적화의 영향을 받는 if-elif-else 구문에서 __cmp__ 호출 횟수를 줄이고 __hash__ 호출을 추가함으로써) 현재 Python의 의미론을 변경해서는 안 됩니다. 이를 보장하기 위해, 스위칭은 “from __future__” 스타일의 플래그를 사용하거나, 스위칭 변수가 내장 불변 타입(int, float, 문자열(string), 유니코드(unicode) 등) 중 하나인 경우에만 안전하게 구현할 수 있습니다(서브타입은 해당되지 않습니다. 서브타입이 여전히 불변인지 아닌지가 명확하지 않기 때문입니다).

점프 테이블 딕셔너리가 나중에 수정되는 것(이는 보호된 코드에 도달하는 데 악용될 수 있습니다)을 막으려면, 점프 테이블은 읽기 전용 타입(예: 읽기 전용 딕셔너리)이어야 합니다.

이 최적화는 최소 n개의 case를 가진 if-elif-else 구문에만 사용해야 합니다(n은 성능 테스트 결과에 따라 앞으로 정해야 할 수입니다).

해결책 2: Python에 switch 문 추가하기

새 문법

switch EXPR:
    case CONSTANT:
        SUITE
    case CONSTANT:
        SUITE
    ...
    else:
        SUITE

(들여쓰기 차이는 무시합니다)

“else” 부분은 선택 사항입니다. else 부분이 주어지지 않았고 정의된 case 중 어느 것도 일치하지 않으면, 아무 동작도 수행하지 않으며 switch 문은 무시됩니다. 이는 현재 if 문의 동작 방식과 일치합니다. 이 상황을 예외로 알리고자 하는 사용자는 else 브랜치를 정의하여 의도한 동작을 구현할 수 있습니다.

상수들이 모두 같은 타입일 필요는 없지만, switch 변수의 타입과 비교할 수 있어야 한다는 점에 유의하십시오.

구현

컴파일러는 이를 다음과 비슷한 바이트코드로 컴파일해야 합니다:

def whatis(x):
    switch(x):
        case 'one':
            print '1'
        case 'two':
            print '2'
        case 'three':
            print '3'
        else:
            print "D'oh!"

이를 다음과 같이 컴파일합니다(POP_TOP과 SET_LINENO는 생략합니다):

   6  LOAD_FAST         0 (x)
   9  LOAD_CONST        1 (switch-table-1)
  12  SWITCH            26 (to 38)

  14  LOAD_CONST        2 ('1')
  17  PRINT_ITEM
  18  PRINT_NEWLINE
  19  JUMP 43

  22  LOAD_CONST        3 ('2')
  25  PRINT_ITEM
  26  PRINT_NEWLINE
  27  JUMP 43

  30  LOAD_CONST        4 ('3')
  33  PRINT_ITEM
  34  PRINT_NEWLINE
  35  JUMP 43

  38  LOAD_CONST        5 ("D'oh!")
  41  PRINT_ITEM
  42  PRINT_NEWLINE

>>43  LOAD_CONST        0 (None)
  46  RETURN_VALUE

여기서 ‘SWITCH’ 옵코드는 ‘x’에 따라 14, 22, 30 또는 38로 점프합니다.

Thomas Wouters가 위 내용을 보여 주는 패치를 작성했습니다. [1]에서 내려받을 수 있습니다.

문제점

switch 문은 (C의 switch 문처럼) 폴스루(fall-through) 동작을 구현해서는 안 됩니다. 각 case는 완전하고 독립적인 스위트를 정의하며, 이는 if-elif-else 문과 매우 비슷합니다. 이렇게 하면 루프 안의 switch 문에서 break를 사용할 수도 있습니다.

인터프리터가 switch 변수 x가 해시 가능하지 않다는 것을 발견하면, 실행 시간에 문제를 알려 주는 TypeError를 발생시켜야 합니다.

기존 키워드를 재사용하여 새 키워드 두 개(“switch”와 “case”)를 추가하지 않는 문법 제안도 있었습니다. 이름은 같지만 의미가 조금 다른(예: break가 없으면 폴스루) C의 키워드와 혼동하지 않도록 새 용어를 키워드로 써야 한다는 주장도 있었습니다. 제안된 변형 중 일부:

case EXPR:
    of CONSTANT:
        SUITE
    of CONSTANT:
        SUITE
    else:
        SUITE

case EXPR:
    if CONSTANT:
         SUITE
    if CONSTANT:
        SUITE
    else:
        SUITE

when EXPR:
    in CONSTANT_TUPLE:
        SUITE
    in CONSTANT_TUPLE:
        SUITE
    ...
else:
     SUITE

switch 문을 확장하여 한 섹션에 여러 값을 허용할 수 있습니다(예: case ‘a’, ‘b’, ‘c’: …). 또 다른 확장 제안은 값의 범위를 허용하는 것입니다(예: case 10..14: …). 이러한 확장은 아마 나중으로 미뤄야 하겠지만, 첫 번째 버전을 설계하고 구현할 때부터 염두에 두어야 합니다.

예시

다음 예제는 모두 해결책 2에서 제안한 새 문법을 사용합니다. 하지만 이 예제는 모두 해결책 1에서도 동작합니다.

switch EXPR:                   switch x:
    case CONSTANT:                 case "first":
        SUITE                          print x
    case CONSTANT:                 case "second":
        SUITE                          x = x**2
    ...                                print x
    else:                          else:
        SUITE                          print "whoops!"


case EXPR:                     case x:
    of CONSTANT:                   of "first":
        SUITE                          print x
    of CONSTANT:                   of "second":
        SUITE                          print x**2
    else:                          else:
        SUITE                          print "whoops!"


case EXPR:                     case state:
    if CONSTANT:                   if "first":
         SUITE                         state = "second"
    if CONSTANT:                   if "second":
        SUITE                          state = "third"
    else:                          else:
        SUITE                          state = "first"


when EXPR:                     when state:
    in CONSTANT_TUPLE:             in ("first", "second"):
        SUITE                          print state
    in CONSTANT_TUPLE:                 state = next_state(state)
        SUITE                      in ("seventh",):
    ...                                print "done"
else:                                  break    # out of loop!
     SUITE                     else:
                                   print "middle state"
                                   state = next_state(state)

다음은 Jack Jansen이 찾아낸 또 다른 좋은 활용 사례입니다(인자 타입에 따라 분기하기):

switch type(x).__name__:
    case 'int':
        SUITE
    case 'string':
        SUITE

범위

XXX “from __future__ import switch”를 설명해야 합니다.

감사의 말

  • Martin von Löwis (최적화 아이디어의 문제점)
  • Thomas Wouters (switch 문 + 바이트코드 컴파일러 예제)
  • Skip Montanaro (디스패치 아이디어, 예제)
  • Donald Beaudry (switch 문법)
  • Greg Ewing (switch 문법)
  • Jack Jansen (타입 스위칭 예제)

참고 문헌