PEP 234 – 이터레이터
- Author:
- Ka-Ping Yee <ping at zesty.ca>, Guido van Rossum <guido at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 30-Jan-2001
- Python-Version:
- 2.1
- Post-History:
- 30-Apr-2001
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 문서는 for 루프의 동작을 제어하기 위해 객체가 제공할 수 있는 이터레이션 인터페이스를 제안합니다. 루프는 이터레이터 객체를 만들어 내는 메서드를 제공하는 방식으로 사용자 정의됩니다. 이터레이터는 다음 값 얻기 연산을 제공합니다. 이 연산은 호출될 때마다 시퀀스의 다음 항목을 만들어 내며, 더 이상 사용할 수 있는 항목이 없으면 예외를 발생시킵니다.
또한 딕셔너리의 키와 파일의 줄을 순회하는 전용 이터레이터를 제안하며, dict.has_key(key)를 key in dict로 쓸 수 있도록 하자는 제안도 합니다.
참고: 이 문서는 두 번째 저자가 이 PEP를 거의 완전히 다시 쓴 것으로, Python 2.2 CVS 트리의 트렁크에 체크인된 실제 구현을 설명합니다. 이 문서는 여전히 논의에 열려 있습니다. 이 PEP의 원래 버전에 있던 다소 난해한 제안 중 일부는 당분간 철회되었습니다. 이들은 향후 별도의 PEP에서 다루어질 수 있습니다.
C API 명세
이터레이션의 끝을 알리는 데 사용할 수 있는 새로운 예외 StopIteration이 정의됩니다.
이터레이터를 요청하기 위한 tp_iter라는 새 슬롯이 타입 객체 구조체에 추가됩니다. 이 슬롯은 PyObject * 인자 하나를 받아 PyObject * 또는 NULL을 반환하는 함수여야 합니다. 이 슬롯을 사용하기 위해 tp_iter 슬롯 함수와 같은 시그니처를 가진 새 C API 함수 PyObject_GetIter()가 추가됩니다.
이터레이션의 다음 값을 얻기 위한 또 다른 새 슬롯 tp_iternext가 타입 구조체에 추가됩니다. 이 슬롯을 사용하기 위해 새 C API 함수 PyIter_Next()가 추가됩니다. NULL 반환 조건은 서로 다르지만, 슬롯과 API 함수의 시그니처는 모두 다음과 같습니다: 인자는 PyObject *이고 반환값도 마찬가지입니다. 반환값이 NULL이 아니면, 그 값은 이터레이션의 다음 값입니다. 반환값이 NULL이면, tp_iternext slot에는 세 가지 경우가 있습니다:
- 예외가 설정되지 않은 경우입니다. 이는 이터레이션의 끝을 의미합니다.
StopIteration예외(또는 파생된 예외 클래스)가 설정된 경우입니다. 이는 이터레이션의 끝을 의미합니다.- 그 밖의 다른 예외가 설정된 경우입니다. 이는 정상적으로 전파되어야 하는 오류가 발생했음을 의미합니다.
상위 수준의 PyIter_Next() 함수는 StopIteration 예외(또는 파생된 예외)가 발생하면 이를 지우므로, 이 함수의 NULL 반환 조건은 더 단순합니다:
- 예외가 설정되지 않은 경우입니다. 이는 이터레이션이 끝났음을 의미합니다.
- 어떤 예외가 설정된 경우입니다. 이는 오류가 발생했으며 정상적으로 전파되어야 함을 의미합니다.
C로 구현된 이터레이터는 tp_iternext 슬롯과 유사한 의미를 가진 next() 메서드를 구현해서는 안 됩니다! 타입의 딕셔너리가 (PyType_Ready()에 의해) 초기화될 때, tp_iternext 슬롯이 있으면 그 슬롯을 감싸는 next() 메서드가 타입의 tp_dict에 추가됩니다. (예외: 타입이 인스턴스 속성에 접근할 때 PyObject_GenericGetAttr()를 사용하지 않으면, 타입의 tp_dict에 있는 next() 메서드가 보이지 않을 수 있습니다.) (이 PEP 원문의 오해로 인해 Python 2.2에서는 모든 이터레이터 타입이 래퍼에 의해 재정의되는 next() 메서드를 구현했습니다. 이 문제는 Python 2.3에서 수정되었습니다.)
바이너리 하위 호환성을 보장하기 위해, 새 플래그 Py_TPFLAGS_HAVE_ITER가 tp_flags 필드의 플래그 집합과 기본 플래그 매크로에 추가됩니다. tp_iter 또는 tp_iternext 슬롯에 접근하기 전에 반드시 이 플래그를 검사해야 합니다. 매크로 PyIter_Check()는 객체에 적절한 플래그가 설정되어 있고 NULL이 아닌 tp_iternext 슬롯이 있는지 검사합니다. tp_iter 슬롯에 대해서는 이러한 매크로가 없습니다(이 슬롯을 참조하는 유일한 곳은 PyObject_GetIter()여야 하며, 이 함수는 Py_TPFLAGS_HAVE_ITER 플래그를 직접 검사할 수 있기 때문입니다).
(참고: tp_iter 슬롯은 어떤 객체에나 있을 수 있지만, tp_iternext 슬롯은 이터레이터 역할을 하는 객체에만 있어야 합니다.)
하위 호환성을 위해, PyObject_GetIter() 함수는 인자가 tp_iter 함수를 구현하지 않은 시퀀스일 때 대체 의미를 구현합니다. 이 경우 시퀀스의 항목을 자연스러운 순서로 순회하는 가벼운 시퀀스 이터레이터 객체가 생성됩니다.
for 루프에 대해 생성되는 Python 바이트코드는 새 옵코드인 GET_ITER와 FOR_ITER를 사용하도록 변경됩니다. 이 옵코드들은 루프 변수의 다음 값을 얻을 때 시퀀스 프로토콜 대신 이터레이터 프로토콜을 사용합니다. 이로써 tp_iter 슬롯을 지원하는, 시퀀스가 아닌 객체도 for 루프로 순회할 수 있게 됩니다. 인터프리터가 시퀀스의 값을 순회하는 다른 곳들도 이터레이터를 사용하도록 변경되어야 합니다.
이터레이터는 tp_iter 슬롯이 자기 자신에 대한 참조를 반환하도록 구현해야 합니다. 이는 (시퀀스가 아닌) 이터레이터를 for 루프에서 사용할 수 있게 하는 데 필요합니다.
(C 또는 Python으로 작성된) 이터레이터 구현은 이터레이터가 한 번 소진을 알린 후에는 이후의 tp_iternext 호출이나 next() 메서드 호출에서도 계속 소진을 알리도록 보장해야 합니다. (StopIteration 이외의) 예외가 발생했을 때 이터레이터가 소진 상태에 들어가야 하는지는 규정하지 않습니다. Python은 사용자 정의 이터레이터나 서드파티 이터레이터가 이 요구 사항을 올바르게 구현한다고 보장할 수 없다는 점에 유의하십시오.
Python API 명세
StopIteration 예외는 표준 예외 중 하나로 노출됩니다. 이 예외는 Exception에서 파생됩니다.
새 내장 함수 iter()가 정의되며, 이 함수는 두 가지 방법으로 호출할 수 있습니다:
iter(obj)는PyObject_GetIter(obj)를 호출합니다.iter(callable, sentinel)은 호출 가능 객체를 호출하여 새 값을 만들고 그 반환값을 센티널 값과 비교하는 특별한 종류의 이터레이터를 반환합니다. 반환값이 센티널과 같으면 이는 반복의 끝을 알리는 것이며, 정상적으로 반환하는 대신StopIteration이 발생합니다. 반환값이 센티널과 같지 않으면 그 값이 이터레이터의 다음 값으로 반환됩니다. 호출 가능 객체가 예외를 일으키면 그 예외는 정상적으로 전파됩니다. 특히 이 함수는 순회를 끝내는 또 다른 방법으로StopIteration을 일으킬 수 있습니다. (이 기능은 C API에서PyCallIter_New(callable, sentinel)로 사용할 수 있습니다.)
두 가지 형태의 iter() 중 어느 것이 반환한 이터레이터 객체든 next() 메서드를 가집니다. 이 메서드는 순회의 다음 값을 반환하거나, 순회의 끝을 알리기 위해 StopIteration(또는 이를 상속한 예외 클래스)을 일으킵니다. 그 밖의 예외는 오류를 뜻하는 것으로 간주해 정상적으로 전파해야 하며, 순회의 끝을 뜻하는 것으로 받아들여서는 안 됩니다.
클래스는 __iter__() 메서드를 정의해 자신이 어떻게 순회될지 정의할 수 있습니다. 이 메서드는 추가 인자를 받지 않고 유효한 이터레이터 객체를 반환해야 합니다. 이터레이터가 되려는 클래스는 두 메서드를 구현해야 합니다. 하나는 위에서 설명한 대로 동작하는 next() 메서드이고, 다른 하나는 self를 반환하는 __iter__() 메서드입니다.
이 두 메서드는 서로 다른 두 프로토콜에 대응합니다:
- 객체가
__iter__()나__getitem__()을 구현하면for로 순회할 수 있습니다. - 객체는
next()함수를 구현하면 이터레이터로 동작할 수 있습니다.
컨테이너 같은 객체는 보통 프로토콜 1을 지원합니다. 현재 이터레이터는 두 프로토콜을 모두 지원해야 합니다. 순회의 의미는 오직 프로토콜 2에서 나옵니다. 프로토콜 1은 이터레이터가 시퀀스처럼 동작하게 하려고 있는 것이며, 특히 이터레이터를 받은 코드가 그 이터레이터에 대해 for 루프를 사용할 수 있게 하기 위한 것입니다.
딕셔너리 이터레이터
- 딕셔너리는
has_key()메서드와 같은 검사를 수행하는sq_contains슬롯을 구현합니다. 이는 다음과 같이 작성할 수 있다는 뜻입니다if k in dict: ...
이는 다음과 동등합니다
if dict.has_key(k): ...
- 딕셔너리는 딕셔너리의 키를 순회하는 효율적인 이터레이터를 반환하는
tp_iter슬롯을 구현합니다. 이러한 순회 중에는 딕셔너리를 수정해서는 안 됩니다. 다만 기존 키에 값을 설정하는 것은 허용됩니다(삭제나 추가는 허용되지 않으며,update()메서드도 허용되지 않습니다). 이는 다음과 같이 작성할 수 있다는 뜻입니다.for k in dict: ...
이는 다음 코드와 동등하지만 훨씬 빠릅니다.
for k in dict.keys(): ...
단, (루프에 의해서든 다른 스레드에 의해서든) 딕셔너리 수정에 대한 제약을 위반하지 않는 경우에 한합니다.
- 서로 다른 종류의 이터레이터를 명시적으로 반환하는 메서드를 딕셔너리에 추가합니다.:
for key in dict.iterkeys(): ... for value in dict.itervalues(): ... for key, value in dict.iteritems(): ...
즉,
for x in dict는for x in dict.iterkeys()의 축약형입니다.
다른 매핑도 이터레이터를 지원한다면 마찬가지로 키를 순회해야 합니다. 그러나 이를 절대적인 규칙으로 받아들여서는 안 됩니다. 특정 애플리케이션에는 다른 요구 사항이 있을 수 있습니다.
파일 이터레이터
다음 제안은 파일의 줄을 순회하는 일반적인 관용구가 보기 흉하고 느리다는 불만에 좋은 해답을 제공하므로 유용합니다.
- 파일은
iter(f.readline, "")와 동등한tp_iter슬롯을 구현합니다. 이는 다음과 같이 작성할 수 있다는 뜻입니다.for line in file: ...
이는 다음 코드의 축약형입니다.
for line in iter(file.readline, ""): ...
이는 다음 코드와 동등하지만 더 빠릅니다.
while 1: line = file.readline() if not line: break ...
이는 또한 일부 이터레이터가 파괴적이라는 점을 보여 줍니다. 이러한 이터레이터는 모든 값을 소비하므로, 같은 값을 독립적으로 순회하는 두 번째 이터레이터를 쉽게 만들 수 없습니다. 파일을 한 번 더 열거나 seek()로 처음으로 되돌아갈 수도 있지만, 이러한 해결책은 모든 파일 유형에서 동작하지는 않습니다. 예를 들어 열린 파일 객체가 실제로는 파이프나 스트림 소켓을 나타내는 경우에는 동작하지 않습니다.
파일 이터레이터는 내부 버퍼를 사용하므로, 이를 다른 파일 연산(예: file.readline())과 섞어 사용하면 올바르게 동작하지 않습니다. 또한 다음 코드는:
for line in file:
if line == "\n":
break
for line in file:
print line,
예상대로 동작하지 않습니다. 두 번째 for 루프가 만든 이터레이터는 첫 번째 for 루프가 미리 읽어 둔 버퍼를 고려하지 않기 때문입니다. 이를 올바르게 작성하는 방법은 다음과 같습니다.:
it = iter(file)
for line in it:
if line == "\n":
break
for line in it:
print line,
(이러한 제약을 두는 근거는 for line in file가 파일의 줄을 순회하는 권장 표준 방법이 되어야 하며, 이 방법은 가능한 한 빨라야 한다는 것입니다. 이터레이터 버전은 이터레이터 내부의 버퍼 덕분에 readline()을 호출하는 것보다 상당히 빠릅니다.)
근거
제안의 모든 부분이 포함된다면, 이는 많은 우려 사항을 일관되고 유연한 방식으로 해결합니다. 주요 장점으로는 다음 네 가지 – 아니, 다섯 가지 – 아니, 여섯 가지 – 요점이 있습니다:
- 확장 가능한 이터레이터 인터페이스를 제공합니다.
- 리스트 순회의 성능을 향상할 수 있습니다.
- 딕셔너리 순회의 성능을 크게 향상할 수 있습니다.
- 요소에 대한 임의 접근을 제공하는 척하지 않고도 순회만을 위한 인터페이스를 제공할 수 있습니다.
- 시퀀스와 매핑을 흉내 내는 기존의 모든 사용자 정의 클래스 및 확장 객체와 하위 호환되며, {
__getitem__,keys,values,items} 중 일부만 구현하는 매핑과도 호환됩니다. - 시퀀스가 아닌 컬렉션을 순회하는 코드를 더 간결하고 읽기 쉽게 만듭니다.
해결된 문제
다음 주제들은 합의 또는 BDFL의 결정으로 확정되었습니다.
next()의 대체 이름으로 두 가지가 제안되었으나 기각되었습니다.__next__()는 타입 객체 슬롯(tp_iternext)에 대응한다는 이유로,__call__()은 이것이 유일한 연산이라는 이유로 제안되었습니다.__next__()에 대한 반대 인자(논거): 많은 이터레이터가 for 루프에서 사용되기는 하지만, 사용자 코드가next()를 직접 호출하는 경우도 있을 것으로 예상되므로__next__()라고 써야 하는 것은 보기 흉합니다. 또한 이 프로토콜을 확장하여prev(),current(),reset()연산을 허용할 수도 있는데, 그때__prev__(),__current__(),__reset__()을 사용하고 싶지는 않을 것이 분명합니다.__call__()(원래 제안)에 대한 반대 인자(논거): 문맥 없이 보면x()는 그다지 읽기 쉽지 않지만x.next()는 명확합니다. 또한 모든 특수 목적 객체가 가장 흔한 연산에__call__()을 사용하려 들어 명확성보다 혼란을 더 많이 초래할 위험이 있습니다.(돌이켜 보면
__next__()를 채택하고,it.__next__()를 호출하는 새 내장 함수next(it)도 두는 편이 더 나았을지도 모릅니다. 하지만 안타깝게도 이미 너무 늦었습니다. 이 방식은 2001년 12월부터 Python 2.2에 배포되어 있습니다.)- 이터레이터를 다시 시작하는 기능을 요청한 사람들이 있습니다. 이는 이터레이터 프로토콜 자체가 아니라 시퀀스에
iter()를 반복해서 호출하는 방식으로 처리해야 합니다. (아래의 요청된 확장도 참고하십시오.) - 반복의 끝을 알리는 데 예외를 사용하는 것이 비용이 너무 크지 않은지 의문이 제기되었습니다.
StopIteration예외의 대안이 여럿 제안되었습니다. 끝을 알리는 특수 값End, 이터레이터가 끝났는지 검사하는 함수end(), 심지어IndexError예외를 재사용하는 방안까지 있었습니다.- 특수 값에는 시퀀스가 그 특수 값을 포함하게 되면 해당 시퀀스에 대한 루프가 아무런 경고 없이 너무 일찍 끝나 버린다는 문제가 있습니다. 널로 끝나는 C 문자열의 경험으로도 이것이 일으킬 수 있는 문제를 깨닫지 못했다면, 특수 값
End가 내장 이름이라고 가정할 때 Python 인트로스펙션 도구가 모든 내장 이름의 목록을 순회하면서 겪을 곤란을 상상해 보십시오! end()함수를 호출하는 방식은 반복마다 두 번의 호출이 필요합니다. 두 번의 호출은 한 번의 호출에 예외 검사를 더한 것보다 훨씬 비용이 큽니다. 특히 시간이 중요한 for 루프는 예외를 매우 저렴하게 검사할 수 있습니다.IndexError를 재사용하면 혼란을 일으킬 수 있습니다. 그것이 진짜 오류일 수도 있는데, 루프가 너무 일찍 끝나면서 그 오류가 가려질 것이기 때문입니다.
- 특수 값에는 시퀀스가 그 특수 값을 포함하게 되면 해당 시퀀스에 대한 루프가 아무런 경고 없이 너무 일찍 끝나 버린다는 문제가 있습니다. 널로 끝나는 C 문자열의 경험으로도 이것이 일으킬 수 있는 문제를 깨닫지 못했다면, 특수 값
- 표준 이터레이터 타입을 요청한 사람들도 있습니다. 그렇게 되면 아마 모든 이터레이터가 이 타입에서 파생되어야 할 것입니다. 하지만 이는 Python다운 방식이 아닙니다. 딕셔너리가 매핑인 것은
__getitem__()과 몇 가지 다른 연산을 지원하기 때문이지, 추상 매핑 타입에서 파생되었기 때문이 아닙니다. if key in dict에 관해서는,x in dict를dict.has_key(x)로 해석하는 것이 단연 가장 유용한 해석이며 아마도 유일하게 유용한 해석이라는 데 의심의 여지가 없습니다.x in list는 x가 값들 가운데 있는지 검사하는 반면, 이 제안은x in dict가 x가 키들 가운데 있는지 검사하도록 만든다는 이유로 반대가 있었습니다. 리스트와 딕셔너리 사이의 대칭성이 매우 약하다는 점을 고려하면, 이 인자(논거)는 그다지 설득력이 없습니다.iter()라는 이름은 약어입니다.iterate(),traverse()같은 대안이 제안되었지만 너무 길어 보입니다. Python에는repr(),str(),len()처럼 자주 쓰는 내장 함수에 약어를 사용해 온 전통이 있습니다.결정:
iter()로 합니다.- 서로 다른 두 연산(객체에서 이터레이터를 얻는 것과, 센티널 값을 사용해 함수에 대한 이터레이터를 만드는 것)에 같은 이름을 쓰는 것은 다소 보기 흉합니다. 하지만 두 번째 연산에 더 나은 이름은 보지 못했으며, 둘 다 이터레이터를 반환하므로 기억하기 쉽습니다.
결정: 내장 함수
iter()는 선택적 인자를 받으며, 이 인자는 찾을 센티널 값입니다. - 특정 이터레이터 객체가 한 번
StopIteration을 발생시키고 나면, 이후의 모든next()호출에서도StopIteration을 발생시킵니까? 이를 필수로 요구하는 것이 유용하다는 의견도 있고, 개별 이터레이터의 재량에 맡겨 두는 것이 유용하다는 의견도 있습니다. 일부 이터레이터 구현(예: 함수를 감싸는 이터레이터)에서는 이를 위해 추가적인 상태 비트가 필요할 수 있다는 점에 유의하십시오.결정: 일단
StopIteration이 발생하면,it.next()를 호출할 때마다 계속해서StopIteration이 발생합니다.참고: 사실 이는 Python 2.2에서 구현되지 않았습니다. 이터레이터의
next()메서드가 한 번의 호출에서는StopIteration을 발생시키지만 다음 호출에서는 발생시키지 않는 경우가 많이 있습니다. 이는 Python 2.3에서 수정되었습니다. - 파일 객체가 그 자신의 이터레이터가 되어,
next()메서드가 다음 줄을 반환하도록 하자는 제안이 있었습니다. 이 방식에는 몇 가지 장점이 있으며, 이 이터레이터가 소모적(destructive)이라는 점을 더욱 분명하게 해 줍니다. 단점은 앞 항목에서 제안한 “끈질긴(sticky) StopIteration” 기능을 구현하기가 훨씬 더 어려워진다는 것입니다.결정: 잠정적으로 거부되었습니다(여전히 이를 지지하는 사람들이 있기는 합니다).
- 일부 사람들은 이터레이터 프로토콜의 확장을 요청했습니다. 예를 들어 이전 항목을 가져오는
prev(), 현재 항목을 다시 가져오는current(), 이터레이터가 끝났는지 검사하는finished(), 그리고 어쩌면rewind(),__len__(),position()같은 다른 것들까지도 요청되었습니다.이 중 일부는 유용하지만, 상당수는 임의의 버퍼링을 추가하지 않고서는 모든 이터레이터 타입에 대해 쉽게 구현할 수 없으며, 때로는 아예 구현할 수 없습니다(또는 합리적인 방법으로는 구현할 수 없습니다). 예를 들어, 파일이나 함수를 순회할 때는 방향을 거꾸로 바꾸는 것과 관련된 어떤 작업도 할 수 없습니다. 이러한 연산이 구현 가능한 경우를 위해 그 이름을 표준화하는 별도의 PEP를 작성할 수도 있을 것입니다.
결정: 거부되었습니다.
- 다음 코드가
for x in dict: ...
x에 딕셔너리의 키, 값, 항목 중 무엇을 차례로 대입해야 하는지에 대해 오랜 논의가 있었습니다.
if x in y와for x in y사이의 대칭성은 키를 순회해야 한다는 것을 시사합니다. 이 대칭성은 많은 사람이 독립적으로 발견했으며, 심지어 하나를 다른 하나로 “설명”하는 데 사용되기도 했습니다. 이는 시퀀스의 경우if x in y가 y를 순회하면서 순회한 값들을 x와 비교하기 때문입니다. 위의 두 제안을 모두 채택하면, 이는 딕셔너리에도 똑같이 성립합니다.for x in dict가 키를 순회하도록 하는 데 반대하는 인자(논거)는 주로 실용성의 관점에서 나옵니다. 표준 라이브러리를 조사해 보면for x in dict.items()의 사용 횟수가for x in dict.keys()의 사용 횟수와 거의 같으며,items()쪽이 근소하게 더 많습니다. 아마도keys()를 사용하는 루프 중 상당수는 어차피dict[x]라고 써서 해당 값을 사용할 것이므로, (그 인자(논거)에 따르면) 키와 값을 모두 제공하면 가장 많은 경우를 지원할 수 있습니다. 이는 사실이지만, 저(Guido)는for x in dict와if x in dict사이의 대응 관계가 깨뜨리기에는 너무 설득력이 있다고 생각하며, 값을 명시적으로 얻기 위해dict[x]를 작성해야 하는 부담도 크지 않습니다.항목을 빠르게 순회하려면
for key, value in dict.iteritems()를 사용하십시오. 저는 다음 코드와for key in dict: dict[key]
다음 코드의
for key, value in dict.iteritems(): pass
시간 차이를 측정해 보았으며, 후자가 약 7%밖에 빠르지 않다는 것을 확인했습니다.
결정: BDFL의 선언에 따라
for x in dict는 키를 순회하며, 딕셔너리는 여러 종류의 딕셔너리 이터레이터를 반환하는iteritems(),iterkeys(),itervalues()를 가집니다.
메일링 리스트
이터레이터 프로토콜은 SourceForge의 메일링 리스트에서 폭넓게 논의되었습니다:
처음에는 논의 중 일부가 Yahoo에서 진행되었으며, 아카이브는 지금도 열람할 수 있습니다:
Copyright
This document is in the public domain.