[Rejected] PEP 204 - Range Literals

원문 링크: PEP 204 - Range Literals

상태: Rejected 유형: Standards Track 작성일: 14-Jul-2000

작성자: Thomas Wouters 상태: Rejected (거부됨) 유형: Standards Track 작성일: 2000년 7월 14일 Python 버전: 2.0


경고: 이 PEP는 거부되었습니다.

신중한 고려와 숙고 끝에 이 제안은 거부되었습니다. Guido van Rossum은 이 제안이 Range와 Slice 구문 사이의 혼란, 그리고 해결되지 않은 문제들로 인해 Python 2.0에 채택하지 않기로 결정했으며, 이후 제안 전체를 거부했습니다. 새로운 구문과 그 의도가 충분히 명확하지 않다고 판단되었습니다.


서론

이 PEP는 Python 2.0을 위한 “Range Literal (범위 리터럴)” 제안에 대해 설명합니다. 이 문서는 이 기능의 상태와 소유권을 추적하며, 기능에 대한 설명과 지원에 필요한 변경 사항을 설명합니다. 또한, 메일링 리스트 포럼에서 논의된 내용을 요약하고 관련 추가 정보 URL을 제공합니다.

리스트 범위 (List ranges)

for 루프에서 자주 사용되는 범위(Range)는 고정된 단계(step)를 가진 숫자 시퀀스입니다. Python의 for 루프는 시퀀스를 직접 순회하도록 설계되었습니다.

>>> l = ['a', 'b', 'c', 'd']
>>> for item in l:
... print item
a
b
c
d

그러나 이 해결책이 항상 적절한 것은 아닙니다. 첫째, for 루프 본문에서 시퀀스를 변경하면 일부 항목이 건너뛰어지는 문제가 발생합니다. 둘째, 시퀀스의 모든 두 번째 요소와 같이 특정 간격으로 순회하는 것이 불가능합니다. 셋째, 때로는 인덱스를 기반으로 요소를 처리해야 하는데, 위 구문에서는 인덱스를 쉽게 사용할 수 없습니다.

이러한 경우를 위해 Python은 숫자로 이루어진 리스트를 생성하는 내장 함수 range()를 제공합니다. range() 함수는 start, end, step 세 가지 인수를 받으며, startstep은 선택 사항이며 각각 0과 1이 기본값입니다.

range() 함수는 start부터 시작하여 step만큼 증가하며 end 직전까지의 숫자로 이루어진 리스트를 생성합니다. 예를 들어, range(10)은 0부터 9까지 총 10개의 숫자를 가진 리스트를 만듭니다.

range() 함수를 사용하면 위의 예시는 다음과 같습니다.

>>> for i in range(len(l)):
... print l[i]
a
b
c
d

혹은 l의 두 번째 요소부터 시작하여 그 이후로는 매 두 번째 요소만 처리하는 경우:

>>> for i in range(1, len(l), 2):
... print l[i]
b
d

이 접근 방식에는 몇 가지 단점이 있습니다.

  • 목적의 명확성 부족: 원하는 리스트의 길이와 단계를 결정하기 위해 추가적인 함수 호출과 계산이 필요할 수 있으며, 이는 코드의 가독성을 떨어뜨립니다. 또한, 내장 range() 함수를 동일한 이름의 지역 또는 전역 변수로 “가려낼(shadow)” 수 있어, 사실상 함수가 대체될 수 있습니다.
  • 비효율성: range() 함수는 오버라이드될 수 있으므로, Python 컴파일러는 for 루프에 대해 가정을 할 수 없으며 별도의 루프 카운터를 유지해야 합니다.
  • 일관성 부족: 아래에 설명된 바와 같이, 이미 범위를 나타내는 구문이 존재하며, 이는 range()와 정확히 동일한 (선택적) 인수를 동일한 방식으로 사용합니다. 이 구문을 범위로 확장하여 “범위 리터럴”을 형성하는 것이 논리적으로 보입니다.

슬라이스 인덱스 (Slice Indices)

Python에서 시퀀스는 단일 항목을 가져오거나 항목의 범위를 가져오는 두 가지 방식으로 인덱싱될 수 있습니다. 항목의 범위를 가져오면 원본 시퀀스와 동일한 유형의 새 객체가 생성되며, 여기에는 원본 시퀀스에서 가져온 0개 이상의 항목이 포함됩니다. 이는 “범위 표기법(range notation)”을 사용하여 수행됩니다.

>>> l[2:4]
['c', 'd']

이 범위 표기법은 콜론으로 구분된 0개, 1개 또는 2개의 인덱스로 구성됩니다. 첫 번째 인덱스는 시작 인덱스이고, 두 번째는 끝 인덱스입니다. 어느 하나라도 생략되면 각각 시퀀스의 시작과 끝이 기본값으로 사용됩니다.

step을 포함하는 확장된 범위 표기법도 있습니다. 이 표기법은 현재 대부분의 내장 타입에서 지원되지 않지만, 만약 지원된다면 다음과 같이 작동할 것입니다.

>>> l[1:4:2]
['b', 'd']

슬라이스 구문의 세 번째 “인수”는 range()step 인수와 정확히 동일합니다. 표준 슬라이스와 확장 슬라이스의 기본 메커니즘은 충분히 다르고 일관성이 없어서 수학 패키지 외의 많은 클래스 및 확장 기능은 확장 변형에 대한 지원을 구현하지 않습니다. 이는 해결되어야 하지만, 이 PEP의 범위 밖입니다.

그러나 확장 슬라이스는 range() 함수 사용의 앞서 언급된 모든 단점을 해결하는 방식으로 범위를 나타내는 완벽하게 유효하고 적용 가능한 구문이 이미 있음을 보여줍니다.

  • 더 명확하고 간결한 구문이며, 이미 직관적이고 배우기 쉽다는 것이 입증되었습니다.
  • Python의 다른 범위 사용(예: 슬라이스)과 일관성이 있습니다.
  • 내장 함수가 아닌 내장 구문이므로 오버라이드될 수 없습니다. 이는 코드를 보는 사람이 코드가 무엇을 하는지 확신할 수 있고, 옵티마이저가 range()가 “가려지는” 것에 대해 걱정할 필요가 없음을 의미합니다.

제안된 해결책

제안된 범위 리터럴 구현은 리스트 리터럴 구문과 (확장) 슬라이스 구문을 결합하여 범위 리터럴을 형성합니다.

>>> [1:10]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [:5]
[0, 1, 2, 3, 4]
>>> [5:1:-1]
[5, 4, 3, 2]

범위 리터럴과 슬라이스 구문 사이에는 한 가지 작은 차이점이 있습니다. 슬라이스에서는 start, end, step 모두를 생략하는 것이 가능하지만, 범위 리터럴에서는 end를 생략하는 것이 의미가 없습니다. 슬라이스에서는 end가 리스트의 끝으로 기본 설정되지만, 이는 범위 리터럴에서는 의미가 없습니다.

참조 구현

제안된 구현은 SourceForge에서 찾을 수 있습니다. 이 구현은 스택에서 세 인수를 가져와 이를 기반으로 리스트를 생성하는 새로운 바이트코드 BUILD_RANGE를 추가합니다. 생성된 리스트는 다시 스택에 푸시됩니다.

새로운 바이트코드를 사용하는 것은 컴파일 시간에 결과가 알려지지 않은 다른 계산을 기반으로 범위를 생성할 수 있도록 하기 위해 필요합니다.

이 코드는 listobject.c에 두 개의 새로운 함수를 도입하는데, 이 함수들은 현재 private 함수와 완전한 API 호출 사이의 경계에 있습니다.

  • PyList_FromRange(): start, end, step으로부터 리스트를 생성하며, 오류 발생 시 NULL을 반환합니다. 프로토타입은 다음과 같습니다.
    PyObject * PyList_FromRange(long start, long end, long step)
    
  • PyList_GetLenOfRange(): 범위의 길이를 결정하는 데 사용되는 헬퍼(helper) 함수입니다. 이전에는 bltinmodule.c의 정적(static) 함수였지만, 이제 listobject.cbltinmodule.c (xrange용) 모두에서 필요합니다. 코드 중복을 피하기 위해 비정적(non-static)으로 만들어졌습니다. 프로토타입은 다음과 같습니다.
    long PyList_GetLenOfRange(long start, long end, long step)
    

미해결 문제 (Open issues)

범위 리터럴에서 end 인수를 필수로 요구하는 불일치에 대한 한 가지 가능한 해결책은 xrange() 내장 함수처럼 범위 구문이 리스트 대신 “제너레이터(generator)”를 생성하도록 허용하는 것입니다. 그러나 제너레이터는 리스트가 아니므로, 예를 들어 제너레이터의 항목에 할당하거나 추가하는 것이 불가능할 것입니다.

범위 구문은 튜플(즉, 불변 리스트)을 포함하도록 확장될 수도 있으며, 이 경우 안전하게 제너레이터로 구현될 수 있습니다. 이는 특히 대규모 숫자 배열에 바람직한 해결책일 수 있습니다. 제너레이터는 저장 및 초기화에 거의 필요하지 않으며, 요청 시 적절한 숫자를 계산하고 생성하는 데 성능에 미치는 영향이 작습니다.

하지만 이 아이디어가 채택되더라도, 구문의 한 인스턴스에서는 두 번째 인수를 선택 사항으로 만들고 다른 경우에는 필수가 아닌 “특별 대우(special case)”를 하는 것이 현명할까요?

범위 구문을 일반 리스트 리터럴과 혼합하여 단일 리스트를 생성할 수 있어야 할까요? 예를 들어:

>>> [5, 6, 1:6, 7, 9]

이것이 다음과 같이 생성되어야 할까요?

[5, 6, 1, 2, 3, 4, 5, 7, 9]

범위 리터럴은 또 다른 제안된 새로운 기능인 “List Comprehension (리스트 컴프리헨션)”과 어떻게 상호 작용해야 할까요? 특히, 리스트 컴프리헨션 내에서 리스트를 생성할 수 있어야 할까요? 예를 들어:

>>> [x:y for x in (1, 2) y in (3, 4)]

이 예시는 여러 범위를 가진 단일 리스트를 반환해야 할까요?

[1, 2, 1, 2, 3, 2, 2, 3]

아니면 다음과 같이 리스트들의 리스트를 반환해야 할까요?

[[1, 2], [1, 2, 3], [2], [2, 3]]

그러나 리스트 컴프리헨션의 구문과 의미는 여전히 뜨거운 논쟁의 대상이므로, 이러한 문제는 “list comprehensions” PEP에서 다루는 것이 가장 좋습니다.

범위 리터럴은 정수 외의 객체도 허용합니다. 전달된 객체에 대해 PyInt_AsLong()을 수행하므로, 객체가 정수로 강제 변환될 수 있는 한 허용됩니다. 그러나 결과 리스트는 항상 표준 정수로 구성됩니다.

범위 리터럴은 전달된 유형의 리스트를 생성해야 할까요? longstring과 같은 다른 내장 유형의 경우에는 바람직할 수 있습니다.

>>> [ 1L : 2L<<64 : 2<<32L ]
>>> ["a":"z":"b"]
>>> ["a":"z":2]

그러나 이것은 너무 많은 “마법”이어서 명확하지 않을 수 있습니다. 또한 사용자 정의 클래스와도 문제가 발생할 수 있습니다. 기본 클래스를 찾고 새 인스턴스를 생성할 수 있더라도, 인스턴스가 __init__에 추가 인수를 요구하여 생성에 실패할 수 있습니다.

PyList_FromRange()PyList_GetLenOfRange() 함수는 분류되어야 합니다. 이들은 API의 일부인가요, 아니면 private 함수로 만들어져야 할까요?

이 문서는 Public Domain에 공개되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments