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

Python 개선 제안 한국어 번역

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 트리의 trunk에 반영된 실제 구현을 설명합니다. 이는 아직 논의가 열려 있습니다. 이 PEP의 원래 버전에 있던 다소 난해한 제안들 중 일부는 현재로서는 철회되었습니다. 이들은 향후 별도의 PEP에서 다룰 대상이 될 수 있습니다.

C API 명세

반복의 끝을 알리는 데 사용할 수 있는 새로운 예외 StopIteration이 정의됩니다.

타입 객체 구조체에 이터레이터를 요청하기 위한 tp_iter라는 새로운 슬롯이 추가됩니다. 이는 PyObject * 인자 하나를 받아 PyObject *를 반환하거나 NULL을 반환하는 함수여야 합니다. 이 슬롯을 사용하기 위해, tp_iter 슬롯 함수와 동일한 시그니처를 가지는 새로운 C API 함수 PyObject_GetIter()가 추가됩니다.

반복에서 다음 값을 얻기 위해, tp_iternext라는 이름의 또 다른 새 슬롯이 타입 구조체에 추가됩니다. 이 슬롯을 사용하기 위해, 새로운 C API 함수 PyIter_Next()가 추가됩니다. 슬롯과 API 함수 모두의 시그니처는 다음과 같지만, NULL 반환 조건은 다릅니다: 인자는 PyObject *이고 반환값도 마찬가지입니다. 반환값이 NULL이 아닐 때, 그것은 반복에서 다음 값입니다. NULL일 때, tp_iternext 슬롯의 경우 세 가지 가능성이 있습니다:

  • 예외가 설정되지 않은 경우; 이는 반복의 끝을 의미합니다.
  • 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에서 수정되었습니다.)

이진 하위 호환성을 보장하기 위해, tp_flags 필드의 플래그 집합과 기본 플래그 매크로에 새 플래그 Py_TPFLAGS_HAVE_ITER가 추가됩니다. tp_iter 또는 tp_iternext 슬롯에 접근하기 전에 이 플래그를 검사해야 합니다. 매크로 PyIter_Check()는 객체가 해당 플래그를 설정하고 있고 tp_iternext 슬롯이 NULL이 아닌지를 검사합니다. tp_iter 슬롯에 대해서는 그러한 매크로가 없습니다(이 슬롯이 참조되는 유일한 곳은 PyObject_GetIter()여야 하고, 이는 Py_TPFLAGS_HAVE_ITER 플래그를 직접 검사할 수 있기 때문입니다).

(참고: tp_iter 슬롯은 어떤 객체에도 존재할 수 있지만, tp_iternext 슬롯은 이터레이터로 동작하는 객체에만 존재해야 합니다.)

하위 호환성을 위해, PyObject_GetIter() 함수는 인자가 tp_iter 함수를 구현하지 않은 시퀀스일 때 대체 시맨틱스를 구현합니다: 이 경우 시퀀스의 항목을 자연스러운 순서로 순회하는 경량 시퀀스 이터레이터 객체가 생성됩니다.

for 루프를 위해 생성되는 파이썬 바이트코드는 루프 변수의 다음 값을 얻기 위해 시퀀스 프로토콜 대신 이터레이터 프로토콜을 사용하는 새로운 연산 코드 GET_ITERFOR_ITER를 사용하도록 변경됩니다. 이로써 tp_iter 슬롯을 지원하는 비시퀀스 객체에 대해서도 for 루프를 사용하여 순회할 수 있게 됩니다. 인터프리터가 시퀀스의 값들을 순회하는 다른 곳들도 이터레이터를 사용하도록 변경해야 합니다.

이터레이터는 tp_iter 슬롯을 자기 자신에 대한 참조를 반환하도록 구현해야 합니다; 이는 (시퀀스가 아니라) 이터레이터를 for 루프에서 사용할 수 있게 하는 데 필요합니다.

(C 또는 파이썬으로 작성된) 이터레이터 구현은 이터레이터가 소진되었음을 알린 후에는 tp_iternextnext() 메서드에 대한 이후 호출도 계속 그렇게 동작할 것을 보장해야 합니다. (StopIteration 이외의) 예외가 발생했을 때 이터레이터가 소진 상태에 들어가야 하는지는 명시되어 있지 않습니다. 파이썬은 사용자 정의 이터레이터나 서드파티 이터레이터가 이 요구 사항을 올바르게 구현하는지 보장할 수 없다는 점에 유의하십시오.

파이썬 API 명세

StopIteration 예외는 표준 예외 중 하나로 노출됩니다. 이는 Exception에서 파생됩니다.

새로운 내장 함수 iter()가 정의되며, 이는 두 가지 방식으로 호출할 수 있습니다:

  • iter(obj)PyObject_GetIter(obj)를 호출합니다.
  • iter(callable, sentinel)은 호출 가능 객체를 호출하여 새로운 값을 생성하고, 그 반환값을 sentinel 값과 비교하는 특수한 종류의 이터레이터를 반환합니다. 반환값이 sentinel과 같으면 이는 반복의 끝을 나타내며 정상적으로 값을 반환하는 대신 StopIteration이 발생합니다. 반환값이 sentinel과 같지 않으면, 이는 이터레이터의 다음 값으로 반환됩니다. 호출 가능 객체가 예외를 발생시키면, 이는 정상적으로 전파됩니다. 특히, 이 함수는 반복을 끝내는 대안적인 방법으로 StopIteration을 발생시키는 것이 허용됩니다. (이 기능은 C API에서 PyCallIter_New(callable, sentinel)로 사용할 수 있습니다.)

iter()의 두 형태 중 어느 쪽으로 반환되든, 이터레이터 객체는 next() 메서드를 가집니다. 이 메서드는 반복의 다음 값을 반환하거나, 반복의 끝을 알리기 위해 StopIteration(또는 그 파생 예외 클래스)을 발생시킵니다. 그 외의 예외는 오류를 의미하는 것으로 간주되어야 하며, 반복의 끝을 의미하는 것으로 받아들이지 말고 정상적으로 전파되어야 합니다.

클래스는 __iter__() 메서드를 정의함으로써 자신이 어떻게 반복되는지를 정의할 수 있습니다. 이 메서드는 추가 인자를 받지 않아야 하며 유효한 이터레이터 객체를 반환해야 합니다. 이터레이터가 되고자 하는 클래스는 두 가지 메서드를 구현해야 합니다: 위에서 설명한 대로 동작하는 next() 메서드와, self를 반환하는 __iter__() 메서드입니다.

이 두 메서드는 서로 다른 두 프로토콜에 대응합니다:

  1. 어떤 객체가 __iter__() 또는 __getitem__()을 구현하면, 그 객체는 for로 반복될 수 있습니다.
  2. 어떤 객체가 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 dictfor 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()을 호출하는 것보다 상당히 빠릅니다.)

근거

제안의 모든 부분이 포함될 경우, 이는 일관되고 유연한 방식으로 많은 우려 사항을 해결합니다. 그 주요 장점 중에는 다음 네 가지 – 아니, 다섯 가지 – 아니, 여섯 가지 요점이 있습니다:

  1. 확장 가능한 이터레이터 인터페이스를 제공합니다.
  2. 리스트 반복에 대한 성능 향상을 가능하게 합니다.
  3. 딕셔너리 반복에 큰 성능 향상을 가능하게 합니다.
  4. 요소에 대한 임의 접근을 제공하는 척하지 않고 반복만을 위한 인터페이스를 제공할 수 있게 합니다.
  5. 시퀀스와 매핑을 흉내 내는 기존의 모든 사용자 정의 클래스 및 확장 객체와 하위 호환되며, {__getitem__, keys, values, items} 중 일부만 구현하는 매핑에 대해서도 마찬가지입니다.
  6. 시퀀스가 아닌 컬렉션을 반복하는 코드를 더 간결하고 읽기 쉽게 만듭니다.

해결된 문제

다음 주제들은 합의 또는 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월부터 파이썬 2.2에 배포되어 있습니다.)

  • 일부 사람들은 이터레이터를 재시작하는 기능을 요청했습니다. 이는 이터레이터 프로토콜 자체가 아니라, 시퀀스에 대해 iter()를 반복적으로 호출함으로써 처리해야 합니다. (아래의 요청된 확장 사항도 참조하십시오.)
  • 반복의 끝을 알리기 위한 예외가 너무 비용이 크지 않은지에 대해 의문이 제기되었습니다. StopIteration 예외에 대한 여러 대안이 제안되었습니다: 끝을 알리는 특수 값 End, 이터레이터가 끝났는지 테스트하는 함수 end(), 심지어 IndexError 예외를 재사용하는 방안까지 있었습니다.
    • 특수 값에는, 시퀀스가 그 특수 값을 포함하게 되면 그 시퀀스에 대한 루프가 아무 경고 없이 조기에 종료된다는 문제가 있습니다. 널로 종료되는 C 문자열의 경험이 이로 인해 발생할 수 있는 문제를 가르쳐주지 않았다면, 특수 End 값이 내장 이름이라고 가정할 때 파이썬 인트로스펙션 도구가 모든 내장 이름의 리스트를 순회하면서 겪게 될 곤란을 상상해 보십시오!
    • end() 함수를 호출하려면 반복마다 두 번의 호출이 필요할 것입니다. 두 번의 호출은 한 번의 호출과 예외 검사를 합친 것보다 훨씬 더 비용이 큽니다. 특히 시간이 중요한 for 루프는 예외를 매우 저렴하게 검사할 수 있습니다.
    • IndexError를 재사용하면, 그것이 진짜 오류일 수 있는데 루프를 조기에 종료함으로써 가려질 수 있으므로 혼란을 일으킬 수 있습니다.
  • 일부는 표준 이터레이터 타입을 요청했습니다. 아마도 모든 이터레이터는 이 타입으로부터 파생되어야 할 것입니다. 하지만 이는 파이썬 방식이 아닙니다: 딕셔너리가 매핑인 이유는 __getitem__()을 비롯한 몇 가지 연산을 지원하기 때문이지, 추상 매핑 타입으로부터 파생되었기 때문이 아닙니다.
  • if key in dict에 관해: x in dictdict.has_key(x)로 해석하는 것이 단연코 가장 유용한 해석이며, 아마도 유일하게 유용한 해석이라는 데는 의심의 여지가 없습니다. x in listx가 값들 중에 존재하는지 검사하는 반면, 이 제안은 x in dictx가 키들 중에 존재하는지 검사하도록 만들기 때문에 이에 대한 반대가 있었습니다. 리스트와 딕셔너리 사이의 대칭성이 매우 약하다는 점을 고려하면, 이 주장은 큰 설득력이 없습니다.
  • iter()라는 이름은 약어입니다. 제안된 대안으로는 iterate(), traverse()가 있지만, 이들은 너무 길어 보입니다. 파이썬에는 repr(), str(), len()처럼 흔한 내장 함수에 약어를 사용해 온 역사가 있습니다.

    결론: iter()로 정해졌습니다.

  • 서로 다른 두 연산(객체에서 이터레이터를 얻는 것과 센티널 값을 가진 함수에 대한 이터레이터를 만드는 것)에 같은 이름을 사용하는 것은 다소 보기 좋지 않습니다. 하지만 두 번째 연산에 더 나은 이름을 찾지 못했으며, 둘 다 이터레이터를 반환하므로 기억하기는 쉽습니다.

    결정 사항: 내장 함수 iter()는 선택적 인자를 받으며, 이는 찾을 센티널 값입니다.

  • 특정 이터레이터 객체가 StopIteration을 일으킨 후, 이후의 모든 next() 호출에서도 StopIteration을 일으켜야 할까요? 어떤 이들은 이를 강제하는 것이 유용하다고 말하고, 다른 이들은 이를 개별 이터레이터의 재량에 맡기는 것이 유용하다고 말합니다. 이는 일부 이터레이터 구현(예: 함수를 감싸는 이터레이터)에서 추가적인 상태 비트를 필요로 할 수 있음에 유의하십시오.

    결정 사항: StopIteration이 일단 일어나면, it.next()를 호출해도 계속 StopIteration을 일으킵니다.

    참고: 이는 실제로 파이썬 2.2에서는 구현되지 않았습니다. 이터레이터의 next() 메서드가 한 번의 호출에서는 StopIteration을 일으키지만 다음 호출에서는 그렇지 않은 경우가 많습니다. 이는 파이썬 2.3에서 개선되었습니다.

  • 파일 객체 자체가 그 자신의 이터레이터가 되어, 다음 줄을 반환하는 next() 메서드를 가져야 한다는 제안이 있었습니다. 이는 몇 가지 장점이 있으며, 이 이터레이터가 소모성(destructive)이라는 점을 더욱 명확하게 합니다. 단점은 이것이 앞선 항목에서 제안된 “고정된(sticky) StopIteration” 기능을 구현하는 것을 훨씬 더 어렵게 만든다는 점입니다.

    결정 사항: 잠정적으로 거부되었습니다(다만 여전히 이를 옹호하는 사람들이 있습니다).

  • 일부 사람들은 이터레이터 프로토콜의 확장을 요청했습니다. 예를 들어, 이전 항목을 얻는 prev(), 현재 항목을 다시 얻는 current(), 이터레이터가 끝났는지 검사하는 finished(), 그리고 어쩌면 rewind(), __len__(), position() 같은 다른 것들도 있습니다.

    이 중 일부는 유용하지만, 상당수는 임의의 버퍼링을 추가하지 않고서는 모든 이터레이터 타입에 대해 쉽게 구현할 수 없으며, 때로는 전혀(또는 합리적으로) 구현할 수 없습니다. 예를 들어, 방향 역전과 관련된 어떤 것도 파일이나 함수를 순회할 때는 수행할 수 없습니다. 이러한 연산이 구현 가능할 때 그 이름을 표준화하기 위해 별도의 PEP를 작성할 수도 있을 것입니다.

    결론: 거부됨.

  • 다음이
    for x in dict: ...
    

    x에 딕셔너리의 연속된 키, 값, 또는 항목 중 무엇을 할당해야 하는지에 대해 오랜 논의가 있었습니다. if x in yfor x in y 사이의 대칭성은 키를 순회해야 함을 시사합니다. 이 대칭성은 많은 사람들이 독립적으로 관찰했으며, 심지어 하나로 다른 하나를 “설명”하는 데 사용되기도 했습니다. 이는 시퀀스의 경우 if x in yy를 순회하면서 순회된 값들을 x와 비교하기 때문입니다. 위의 두 제안을 모두 채택한다면, 이는 딕셔너리에 대해서도 성립하게 됩니다.

    for x in dict가 키를 순회하도록 만드는 것에 반대하는 논거는 주로 실용성 관점에서 나옵니다. 표준 라이브러리를 조사해 보면 for x in dict.items()의 사용 빈도가 for x in dict.keys()와 거의 비슷하며, items() 버전이 약간 더 많다는 것을 알 수 있습니다. 아마도 keys()를 사용하는 루프의 상당수는 dict[x]를 작성함으로써 어차피 대응하는 값을 사용하고 있을 것이므로, (이 논거에 따르면) 키와 값을 모두 제공함으로써 가장 많은 경우를 지원할 수 있다는 것입니다. 이것이 사실이기는 하지만, 저(Guido)는 for x in dictif 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에서 이루어졌으며, 아카이브는 여전히 접근 가능합니다: