[Final] PEP 234 - Iterators

원문 링크: PEP 234 - Iterators

상태: Final 유형: Standards Track 작성일: 30-Jan-2001

PEP 234 – Iterators

  • 작성자: Ka-Ping Yee, Guido van Rossum
  • 상태: Final (최종)
  • 유형: Standards Track
  • 생성일: 2001년 1월 30일
  • Python 버전: 2.1 (Python 2.2에 구현됨)

초록 (Abstract)

이 문서는 for 루프의 동작을 제어하기 위해 객체가 제공할 수 있는 이터레이션(iteration) 인터페이스를 제안합니다. 루프 동작은 이터레이터 객체를 생성하는 메서드를 제공함으로써 사용자 정의될 수 있습니다. 이터레이터는 호출될 때마다 시퀀스(sequence)의 다음 항목을 생성하는 get next value 연산을 제공하며, 더 이상 항목이 없을 때는 예외를 발생시킵니다.

또한, 딕셔너리의 키(keys)와 파일의 라인(lines)에 대한 특정 이터레이터를 제안하며, dict.has_key(key)key in dict로 표현할 수 있도록 하는 제안도 포함합니다.

참고: 이 PEP는 두 번째 저자(Guido van Rossum)에 의해 거의 전면적으로 다시 작성되었으며, Python 2.2 CVS 트렁크에 체크인된 실제 구현을 설명합니다. 초기 버전의 일부 난해한 제안들은 현재 철회되었으며, 나중에 별도의 PEP의 주제가 될 수 있습니다.

C API 명세 (C API Specification)

  • StopIteration 예외 정의: 이터레이션의 끝을 알리는 새로운 예외 StopIteration이 정의되었습니다.
  • tp_iter 슬롯: 이터레이터를 요청하기 위한 새로운 슬롯 tp_iter가 타입(type) 객체 구조에 추가되었습니다. 이 슬롯은 PyObject * 인수를 하나 받고 PyObject *를 반환하거나 NULL을 반환하는 함수여야 합니다. 이 슬롯을 사용하기 위해 PyObject_GetIter()라는 새로운 C API 함수가 추가되었습니다.
  • tp_iternext 슬롯: 이터레이션의 다음 값을 얻기 위한 새로운 슬롯 tp_iternext가 타입 구조에 추가되었습니다. 이 슬롯을 사용하기 위해 PyIter_Next()라는 새로운 C API 함수가 추가되었습니다. tp_iternext 슬롯이 NULL을 반환하는 경우, 다음과 같은 세 가지 가능성이 있습니다.
    • 예외가 설정되지 않음: 이터레이션의 끝을 의미합니다.
    • StopIteration 예외(또는 파생 클래스)가 설정됨: 이터레이션의 끝을 의미합니다.
    • 다른 예외가 설정됨: 일반적인 오류 발생을 의미합니다.

    상위 레벨 함수인 PyIter_Next()StopIteration 예외가 발생하면 이를 지우므로, NULL 반환 조건은 더 간단합니다.

    • 예외가 설정되지 않음: 이터레이션이 끝났음을 의미합니다.
    • 예외가 설정됨: 오류가 발생했음을 의미하며, 일반적으로 전파되어야 합니다.
  • next() 메서드 자동 생성: C로 구현된 이터레이터는 tp_iternext 슬롯과 유사한 의미를 가진 next() 메서드를 직접 구현해서는 안 됩니다. PyType_Ready()에 의해 타입의 딕셔너리가 초기화될 때, tp_iternext 슬롯의 존재는 해당 슬롯을 래핑(wrapping)하는 next() 메서드가 타입의 tp_dict에 추가되도록 합니다.
  • Py_TPFLAGS_HAVE_ITER 플래그: 바이너리 하위 호환성을 보장하기 위해 tp_flags 필드에 Py_TPFLAGS_HAVE_ITER라는 새로운 플래그가 추가되었습니다. tp_iter 또는 tp_iternext 슬롯에 접근하기 전에 이 플래그를 확인해야 합니다.
  • 시퀀스 객체를 위한 폴백(Fallback): PyObject_GetIter() 함수는 인수가 tp_iter 함수를 구현하지 않는 시퀀스일 경우, 폴백(fallback) 의미론을 구현합니다. 이 경우 가벼운(lightweight) 시퀀스 이터레이터 객체가 구성되어 시퀀스의 항목들을 자연스러운 순서로 이터레이트합니다.
  • 바이트코드 변경: for 루프에 대해 생성되는 Python 바이트코드는 새로운 opcode인 GET_ITERFOR_ITER를 사용하도록 변경되었습니다. 이는 루프 변수의 다음 값을 가져오기 위해 시퀀스 프로토콜 대신 이터레이터 프로토콜을 사용합니다.
  • 자기 자신을 반환하는 tp_iter: 이터레이터는 tp_iter 슬롯이 자기 자신에 대한 참조를 반환하도록 구현해야 합니다. 이는 for 루프에서 시퀀스가 아닌 이터레이터를 사용할 수 있게 하기 위해 필요합니다.
  • StopIteration 이후 동작: 이터레이터 구현(C 또는 Python)은 이터레이터가 소진(exhaustion)을 알린 후에는 tp_iternext 또는 next() 메서드에 대한 후속 호출도 계속해서 소진을 알려야 함을 보장해야 합니다.

Python API 명세 (Python API Specification)

  • StopIteration 예외: StopIteration 예외는 표준 예외 중 하나로 노출되며, Exception을 상속합니다.
  • iter() 내장 함수: iter()라는 새로운 내장 함수가 정의되었으며, 두 가지 방식으로 호출할 수 있습니다.
    • iter(obj): PyObject_GetIter(obj)를 호출합니다.
    • iter(callable, sentinel): callable을 호출하여 새로운 값을 생성하고, 반환 값을 sentinel 값과 비교하는 특별한 종류의 이터레이터를 반환합니다. 반환 값이 sentinel과 같으면 이터레이션의 끝을 알리며 StopIteration이 발생합니다. 같지 않으면 다음 값으로 반환됩니다. callable이 예외를 발생시키면 정상적으로 전파됩니다.
  • 이터레이터 객체의 next() 메서드: iter() 함수에 의해 반환된 이터레이터 객체는 next() 메서드를 가집니다. 이 메서드는 이터레이션의 다음 값을 반환하거나, 이터레이션의 끝을 알리기 위해 StopIteration 예외를 발생시킵니다. 다른 예외는 오류를 나타내며 정상적으로 전파되어야 합니다.
  • 사용자 정의 이터러블 및 이터레이터:
    • 클래스는 __iter__() 메서드를 정의하여 이터레이션 방식을 정의할 수 있습니다. 이 메서드는 추가 인수를 받지 않고 유효한 이터레이터 객체를 반환해야 합니다.
    • 이터레이터가 되고자 하는 클래스는 두 가지 메서드를 구현해야 합니다: 위에서 설명한 대로 동작하는 next() 메서드와 self를 반환하는 __iter__() 메서드.
  • 두 가지 프로토콜:
    • __iter__() 또는 __getitem__()을 구현하는 객체는 for 루프를 통해 이터레이트될 수 있습니다.
    • next()를 구현하는 객체는 이터레이터로 기능할 수 있습니다. 컨테이너류 객체는 일반적으로 첫 번째 프로토콜을 지원합니다. 이터레이터는 현재 두 프로토콜을 모두 지원해야 합니다.

딕셔너리 이터레이터 (Dictionary Iterators)

  • key in dict 구문: 딕셔너리는 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()의 축약형입니다.

파일 이터레이터 (File Iterators)

파일 객체에 대한 이터레이터 제안은 파일의 라인(line)을 이터레이트하는 일반적인 관용구(idiom)가 보기 흉하고 느리다는 불만에 대한 좋은 해결책을 제공합니다.

  • 파일 라인 이터레이션: 파일은 iter(f.readline, "")와 동등한 tp_iter 슬롯을 구현합니다. 이는 for line in file: ...와 같이 작성할 수 있음을 의미하며, while 1: line = file.readline(); if not line: break; ...보다 빠릅니다.
  • 파괴적인(Destructive) 이터레이터: 일부 이터레이터는 파괴적(destructive)입니다. 즉, 모든 값을 소비하며, 동일한 값을 독립적으로 이터레이트하는 두 번째 이터레이터를 쉽게 생성할 수 없습니다. 파일을 다시 열거나 seek()를 사용하여 처음으로 이동할 수 있지만, 파이프(pipe)나 스트림 소켓(stream socket)과 같은 일부 파일 유형에서는 이러한 해결책이 작동하지 않습니다.
  • 내부 버퍼링 및 제약 사항: 파일 이터레이터는 내부 버퍼를 사용하므로, 이를 다른 파일 연산(file.readline() 등)과 혼합하면 올바르게 작동하지 않을 수 있습니다. 예를 들어, 두 개의 연속된 for 루프에서 파일 이터레이터를 사용할 때, 첫 번째 루프가 읽어들인 버퍼를 두 번째 루프가 고려하지 않아 예상과 다르게 동작할 수 있습니다. 올바른 사용법은 이터레이터 객체를 변수에 할당하여 재사용하는 것입니다.

    it = iter(file)
    for line in it:
        if line == "\n":
            break
    for line in it:
        print(line)
    

    이러한 제약 사항의 이유는 for line in file이 파일을 라인별로 이터레이트하는 권장되고 표준적인 방법이 되어야 하며, 가능한 한 빨라야 하기 때문입니다. 이터레이터 버전은 이터레이터 내부 버퍼 덕분에 readline()을 호출하는 것보다 훨씬 빠릅니다.

제안 배경 (Rationale)

이 제안의 모든 부분이 포함되면, 일관되고 유연한 방식으로 많은 우려 사항을 해결합니다. 주요 장점은 다음과 같습니다.

  • 확장 가능한 이터레이터 인터페이스를 제공합니다.
  • 리스트(list) 이터레이션의 성능 향상을 가능하게 합니다.
  • 딕셔너리(dictionary) 이터레이션의 큰 성능 향상을 가능하게 합니다.
  • 요소에 대한 무작위 접근(random access)을 제공하는 척하지 않고, 단순히 이터레이션만을 위한 인터페이스를 제공할 수 있게 합니다.
  • 시퀀스 및 매핑을 에뮬레이트하는 모든 기존 사용자 정의 클래스 및 확장 객체와 하위 호환됩니다.
  • 시퀀스가 아닌 컬렉션(non-sequence collections)을 이터레이트하는 코드를 더 간결하고 읽기 쉽게 만듭니다.

해결된 문제 (Resolved Issues)

다음 주제들은 합의 또는 BDFL(Benevolent Dictator For Life, Guido van Rossum)의 결정에 따라 해결되었습니다.

  • next() 메서드 이름: next()에 대한 두 가지 대안적 철자(__next__(), __call__())가 제안되었지만 거부되었습니다.
    • __next__() 반대 의견: for 루프에서 많이 사용되지만, 사용자 코드가 next()를 직접 호출할 수도 있으므로 __next__()는 보기 좋지 않습니다. 또한, prev(), current(), reset()과 같은 작업으로 프로토콜을 확장할 가능성이 있는데, 이때 __prev__(), __current__(), __reset__()과 같은 이름은 원하지 않을 것입니다.
    • __call__() 반대 의견 (원래 제안): 문맥을 벗어나면 x()는 읽기 어렵지만, x.next()는 명확합니다. 모든 특수 목적 객체가 가장 일반적인 연산을 위해 __call__()을 사용하려 할 위험이 있으며, 이는 명확성보다 더 많은 혼란을 야기할 수 있습니다.
    • 결정: next()를 사용합니다. (회고적으로는 __next__()를 사용하고 next(it)와 같은 새로운 내장 함수를 두는 것이 더 좋았을 수도 있지만, Python 2.2에 이미 배포되어 너무 늦었습니다.)
  • 이터레이터 재시작: 이터레이터를 재시작하는 기능이 요청되었지만, 이는 시퀀스에 대해 iter()를 반복적으로 호출하여 처리해야 하며, 이터레이터 프로토콜 자체를 통해서는 처리하지 않기로 결정되었습니다.
  • StopIteration 예외 비용: StopIteration 예외가 너무 비싸지 않냐는 의문이 제기되었습니다. StopIteration 예외에 대한 여러 대안(특수 값 End, 이터레이터가 끝났는지 테스트하는 함수 end(), IndexError 재사용)이 제안되었습니다.
    • 특수 값 End의 문제점: 시퀀스가 그 특수 값을 포함할 경우, 루프가 경고 없이 조기에 종료될 수 있습니다.
    • end() 함수 호출의 문제점: 이터레이션당 두 번의 호출이 필요하며, 이는 예외 테스트보다 훨씬 비쌉니다.
    • IndexError 재사용의 문제점: 진정한 오류일 수 있는 IndexError가 루프를 조기에 종료함으로써 가려질 수 있어 혼란을 야기할 수 있습니다.
    • 결정: StopIteration 예외를 사용합니다.
  • 표준 이터레이터 타입: 모든 이터레이터가 파생되어야 하는 표준 이터레이터 타입에 대한 요청이 있었지만 거부되었습니다. 이는 Python의 방식이 아니라고 판단되었습니다.
  • key in dict의 의미: dict.has_key(x)와 같은 x in dict의 해석이 가장 유용하다고 판단되었습니다. x in list가 값의 존재 여부를 확인하는 반면, x in dict가 키의 존재 여부를 확인하는 것에 대한 반대가 있었지만, 리스트와 딕셔너리 간의 대칭성이 약하므로 이 주장은 큰 의미가 없다고 결론지었습니다.
  • iter() 이름: iter()는 축약형이며 iterate(), traverse()와 같은 대안이 제안되었지만 너무 길게 느껴졌습니다. Python은 repr(), str(), len()과 같이 일반적인 내장 함수에 축약형을 사용한 역사가 있습니다.
    • 결정: iter()를 사용합니다.
    • 두 가지 다른 연산(객체에서 이터레이터를 가져오는 것과 센티널 값을 가진 함수를 위한 이터레이터를 만드는 것)에 동일한 이름을 사용하는 것이 다소 보기 흉하다는 의견이 있었지만, 두 연산 모두 이터레이터를 반환하므로 기억하기 쉽다는 이유로 유지되었습니다.
    • 결정: 내장 함수 iter()는 찾을 센티널(sentinel) 값인 선택적 인수를 받습니다.
  • StopIterationnext() 호출: 특정 이터레이터 객체가 StopIteration을 한 번 발생시킨 후, 후속 next() 호출에서도 계속 StopIteration을 발생시켜야 하는지에 대한 논의가 있었습니다.
    • 결정: StopIteration이 발생한 후에는 it.next()를 호출해도 계속 StopIteration이 발생합니다. (Python 2.2에서는 구현되지 않았지만 Python 2.3에서 수정되었습니다.)
  • 파일 객체가 자기 자신 이터레이터: 파일 객체가 자체적으로 next() 메서드를 가진 이터레이터가 되어야 한다는 제안이 있었지만, 이는 “끈적한 StopIteration (sticky StopIteration)” 기능을 구현하기 더 어렵게 만들 수 있다는 단점 때문에 잠정적으로 거부되었습니다.
  • 이터레이터 프로토콜 확장 (prev(), current(), rewind() 등): prev(), current(), finished(), rewind(), __len__(), position() 등 이터레이터 프로토콜 확장에 대한 요청이 있었지만, 많은 경우 임의의 버퍼링을 추가하지 않고는 쉽게 구현할 수 없거나 전혀 합리적으로 구현할 수 없기 때문에 거부되었습니다.
  • for x in dict의 의미: for x in dict:가 딕셔너리의 키, 값 또는 항목 중 무엇을 할당해야 하는지에 대한 긴 논의가 있었습니다. if x in yfor x in y 사이의 대칭성은 키를 이터레이트해야 함을 시사했습니다. 실용적인 관점에서 dict.items()dict.keys() 사용이 거의 비슷하다는 점, 그리고 dict.keys()를 사용하는 많은 루프가 결국 dict[x]를 통해 해당 값을 사용하는 점을 들어 항목(키와 값)을 이터레이트하는 것이 더 많은 경우를 지원할 수 있다는 주장이 있었습니다. 하지만 Guido van Rossum은 for x in dictif x in dict 사이의 일관성이 매우 중요하다고 판단했습니다.
    • 결정 (BDFL 결정): for x in dict는 키를 이터레이트하며, 딕셔너리는 다른 종류의 딕셔너리 이터레이터를 반환하기 위해 iteritems(), iterkeys(), itervalues()를 가집니다. for key, value in dict.iteritems():를 사용하면 항목(items)에 대한 빠른 이터레이션이 가능합니다.

이메일 목록 (Mailing Lists)

이터레이터 프로토콜은 SourceForge의 다음 메일링 리스트에서 광범위하게 논의되었습니다.

  • http://lists.sourceforge.net/lists/listinfo/python-iterators
  • 초기에는 Yahoo에서 일부 논의가 이루어졌으며, 아카이브는 여전히 접근 가능합니다.
    • http://groups.yahoo.com/group/python-iter

이 문서는 퍼블릭 도메인(public domain)에 있습니다.

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

Comments