PEP 204 – 범위 리터럴
- Author:
- Thomas Wouters <thomas at python.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 14-Jul-2000
- Python-Version:
- 2.0
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
소개
이 PEP는 Python 2.0을 위한 “범위 리터럴” 제안을 설명합니다. 이 PEP는 Python 2.0에 도입될 예정인 이 기능의 상태와 담당자를 추적합니다. 이 PEP에는 기능에 대한 설명과 기능을 지원하는 데 필요한 변경 사항의 개요가 담겨 있습니다. 이 PEP는 메일링 리스트 포럼에서 이루어진 논의를 요약하며, 필요한 경우 추가 정보를 위한 URL을 제공합니다. 이 파일의 CVS 리비전 이력에 확정적인 역사 기록이 담겨 있습니다.
리스트 범위
범위는 일정한 간격을 가진 수의 시퀀스로, for 루프에서 자주 사용됩니다. Python의 for 루프는 시퀀스를 직접 순회하도록 설계되었습니다:
>>> l = ['a', 'b', 'c', 'd']
>>> for item in l:
... print item
a
b
c
d
그러나 이 방법이 항상 현명한 것은 아닙니다. 첫째, for 루프 본문에서 시퀀스를 변경하면 문제가 발생하여 for 루프가 항목을 건너뛰게 됩니다. 둘째, 예를 들어 시퀀스의 요소를 하나 걸러 하나씩 순회하는 것이 불가능합니다. 셋째, 때로는 요소를 인덱스에 따라 처리해야 하는데, 위 구조에서는 인덱스를 바로 얻을 수 없습니다.
이러한 경우와 그 밖에 수의 범위가 필요한 경우를 위해 Python은 수의 리스트를 생성하는 range 내장 함수를 제공합니다. range 함수는 start, end, step 세 개의 인자를 받습니다. start와 step은 선택 사항이며, 기본값은 각각 0과 1입니다.
range 함수는 start에서 시작하여 step 간격으로, end는 포함하지 않고 그 직전까지의 숫자 리스트를 생성합니다. 따라서 range(10)은 정확히 10개의 항목, 즉 숫자 0부터 9까지를 담은 리스트를 만듭니다.
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 루프에 대해 어떠한 가정도 할 수 없으며, 별도의 루프 카운터를 유지해야 합니다. - 일관성: 아래와 같이 범위를 나타내는 데 사용되는 문법이 이미 존재합니다. 이 문법은 모두 선택 사항이기는 하지만 정확히 같은 인자를 정확히 같은 방식으로 사용합니다. 이 문법을 범위로 확장하여 “범위 리터럴”을 만드는 것은 논리적으로 보입니다.
슬라이스 인덱스
Python에서 시퀀스를 인덱싱하는 방법에는 두 가지가 있습니다. 하나는 단일 항목을 가져오는 것이고, 다른 하나는 항목의 범위를 가져오는 것입니다. 항목의 범위를 가져오면 원래 시퀀스와 같은 타입의 새 객체가 만들어지며, 이 객체에는 원래 시퀀스의 항목이 0개 이상 들어 있습니다. 이는 “범위 표기법”을 사용하여 수행됩니다:
>>> l[2:4]
['c', 'd']
이 범위 표기법은 콜론으로 구분된 0개, 1개 또는 2개의 인덱스로 이루어집니다. 첫 번째 인덱스는 start 인덱스이고, 두 번째 인덱스는 end 인덱스입니다. 둘 중 하나가 생략되면 각각 시퀀스의 시작과 끝이 기본값이 됩니다.
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 [1]에서 찾을 수 있습니다. 이 구현은 새로운 바이트코드 BUILD_RANGE를 추가하는데, 이 바이트코드는 스택에서 세 개의 인자를 가져와 이를 바탕으로 리스트를 만듭니다. 만들어진 리스트는 다시 스택에 푸시됩니다.
컴파일 시점에 결과를 알 수 없는 다른 계산을 바탕으로 범위를 만들 수 있으려면 새로운 바이트코드를 사용해야 합니다.
이 코드는 listobject.c에 두 개의 새 함수를 도입하는데, 이 함수들은 현재 비공개 함수와 완전한 API 호출 사이의 어중간한 위치에 있습니다.
PyList_FromRange()는 start, end, step으로 리스트를 만들며, 오류가 발생하면 NULL을 반환합니다. 그 프로토타입은 다음과 같습니다:
PyObject * PyList_FromRange(long start, long end, long step)
PyList_GetLenOfRange()는 범위의 길이를 구하는 데 사용되는 헬퍼 함수입니다. 이전에는 bltinmodule.c의 정적 함수였지만, 이제는 listobject.c와 bltinmodule.c 양쪽 모두에서(xrange를 위해) 필요합니다. 오로지 코드 중복을 피하기 위해 이 함수를 비정적(non-static) 함수로 만들었습니다. 이 함수의 프로토타입은 다음과 같습니다.:
long PyList_GetLenOfRange(long start, long end, long step)
미해결 문제
- 범위 리터럴에서 end 인자를 요구하는 불일치를 해결할 수 있는 한 가지 방법은,
xrange내장 함수처럼 범위 문법이 리스트 대신 “제너레이터”를 생성하도록 허용하는 것입니다. 그러나 제너레이터는 리스트가 아니므로, 예를 들어 제너레이터의 항목에 값을 대입하거나 제너레이터에 항목을 추가하는 것은 불가능합니다.범위 문법을 튜플(즉, 불변 리스트)까지 포함하도록 확장하는 것도 생각해 볼 수 있으며, 그러면 튜플은 제너레이터로 안전하게 구현할 수 있습니다. 이는 특히 큰 숫자 배열의 경우 바람직한 해결책일 수 있습니다. 제너레이터는 저장 공간과 초기화 비용이 거의 들지 않으며, 요청 시 적절한 숫자를 계산하고 생성하는 데 따르는 성능 영향도 작습니다. (미정: 성능 영향이 조금이라도 있습니까? 간단히 테스트해 본 결과, 길이가 1인 범위의 경우에도 성능이 같은 것으로 보입니다.)
그러나 이 아이디어를 채택한다고 하더라도, 두 번째 인자를 “특수하게 처리”하여 문법의 한 경우에서는 선택 사항으로, 다른 경우에서는 필수로 만드는 것이 현명하겠습니까?
- 범위 문법과 일반 리스트 리터럴을 섞어 하나의 리스트를 만들 수 있어야 하겠습니까? 예를 들면 다음과 같습니다.:
>>> [5, 6, 1:6, 7, 9]
이를 통해 다음을 생성하는 것입니다.:
[5, 6, 1, 2, 3, 4, 5, 7, 9]
- 범위 리터럴은 제안된 또 다른 새 기능인 “리스트 컴프리헨션”과 어떻게 상호 작용해야 합니까? 구체적으로, 리스트 컴프리헨션 안에서 리스트를 생성할 수 있어야 합니까? 예를 들면 다음과 같습니다.:
>>> [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]]
그러나 리스트 컴프리헨션의 문법과 의미는 여전히 뜨거운 논쟁의 대상이므로, 이러한 문제는 “리스트 컴프리헨션” PEP에서 다루는 것이 가장 좋을 것입니다.
- 범위 리터럴은 정수 이외의 객체도 받아들입니다. 전달된 객체에
PyInt_AsLong()을 수행하므로, 정수로 강제 변환될 수 있는 객체라면 받아들여집니다. 그러나 결과 리스트는 항상 표준 정수로 구성됩니다.범위 리터럴이 전달된 타입의 리스트를 생성해야 합니까? long이나 문자열 같은 다른 내장 타입의 경우에는 이것이 바람직할 수 있습니다.:
>>> [ 1L : 2L<<64 : 2<<32L ] >>> ["a":"z":"b"] >>> ["a":"z":2]
그러나 이는 명확하게 이해하기에는 지나치게 “마법” 같은 동작일 수 있습니다. 또한 사용자 정의 클래스에서 문제가 생길 수도 있습니다. 베이스 클래스를 찾아 새 인스턴스를 생성할 수 있더라도, 해당 인스턴스가
__init__에 추가 인자를 요구하여 생성이 실패할 수 있습니다. PyList_FromRange()와PyList_GetLenOfRange()함수를 분류해야 합니다. 이 함수들은 API의 일부입니까, 아니면 비공개 함수로 만들어야 합니까?
Copyright
This document has been placed in the Public Domain.