PEP 201 – 병렬 반복
- Author:
- Barry Warsaw <barry at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 13-Jul-2000
- Python-Version:
- 2.0
- Post-History:
- 27-Jul-2000
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
소개
이 PEP는 ‘병렬 반복(lockstep iteration)’ 제안을 설명합니다. 이 PEP는 Python 2.0에 도입될 예정인 이 기능의 상태와 담당자를 추적합니다. 이 문서는 기능을 설명하고, 기능을 지원하는 데 필요한 변경 사항의 개요를 제시합니다. 이 PEP는 메일링 리스트 포럼에서 있었던 논의를 요약하고, 필요한 경우 추가 정보를 얻을 수 있는 URL을 제공합니다. 확정적인 역사 기록은 이 파일의 CVS 리비전 기록에 담겨 있습니다.
동기
Python의 표준 for 루프는 시퀀스가 소진될 때까지 시퀀스의 모든 요소를 순회합니다 [1]. 그러나 for 루프는 단 하나의 시퀀스만 순회하며, 둘 이상의 시퀀스를 잠금 단계(lock-step) 방식으로 함께 순회하는 것이 바람직한 경우가 많습니다. 다시 말해, 루프의 i번째 반복이 각 시퀀스의 i번째 요소를 담은 객체를 반환하는 방식으로 순회하는 것입니다.
이를 위해 흔히 사용하는 관용구는 직관적이지 않습니다. 이 PEP는 zip이라는 새 내장 함수를 도입하여 이러한 반복을 수행하는 표준 방법을 제안합니다.
zip()의 주된 동기는 잠금 단계(lock-step) 순회에서 비롯되지만, zip()을 내장 함수로 구현함으로써 for 루프 이외의 맥락에서도 추가적인 유용성을 갖게 됩니다.
병렬 for 루프
병렬 for 루프는 둘 이상의 시퀀스에 대한 중첩되지 않은 반복으로, 루프를 한 번 돌 때마다 각 시퀀스에서 요소를 하나씩 가져와 대상을 구성합니다. 이 동작은 Python에서 이미 map() 내장 함수를 사용하여 구현할 수 있습니다:
>>> a = (1, 2, 3)
>>> b = (4, 5, 6)
>>> for i in map(None, a, b): print i
...
(1, 4)
(2, 5)
(3, 6)
>>> map(None, a, b)
[(1, 4), (2, 5), (3, 6)]
for 루프는 평소처럼 이 리스트를 순회하기만 하면 됩니다.
map() 관용구는 Python에서 흔히 쓰이지만, 몇 가지 단점이 있습니다:
- 함수형 프로그래밍 배경이 없는 프로그래머에게는 명확하지 않습니다.
- 마법 같은
None을 첫 번째 인자로 사용하는 방식이 명확하지 않습니다. - 리스트의 길이가 서로 다를 때 임의적이고, 흔히 의도하지 않은, 유연하지 못한 의미를 가집니다: 더 짧은 시퀀스는
None으로 채워집니다:>>> c = (4, 5, 6, 7) >>> map(None, a, c) [(1, 4), (2, 5), (3, 6), (None, 7)]
이러한 이유로 Python 2.0 베타 기간에 병렬 for 루프를 문법 차원에서 지원하자는 몇 가지 제안이 나왔습니다. 다음은 그중 두 가지 제안입니다:
for x in seq1, y in seq2:
# stuff
for x, y in seq1, seq2:
# stuff
두 형식 모두 Python에서 이미 의미를 가지고 있고, 그 의미를 바꾸면 기존 코드가 깨지므로 둘 다 사용할 수 없습니다. 새 문법에 대한 다른 모든 제안도 같은 문제를 겪었거나, ‘리스트 컴프리헨션’이라는 또 다른 제안 기능과 충돌했습니다(PEP 202 참고).
제안하는 해결책
제안하는 해결책은 __builtin__ 모듈에서 사용할 수 있는 새로운 내장 시퀀스 제너레이터 함수를 도입하는 것입니다. 이 함수의 이름은 zip이며 다음과 같은 시그니처를 가집니다:
zip(seqa, [seqb, [...]])
zip()은 하나 이상의 시퀀스를 받아 그 요소들을 엮는데, 이는 길이가 같은 시퀀스에 대해 map(None, ...)이 하는 것과 같습니다. 엮기는 가장 짧은 시퀀스가 소진되면 멈춥니다.
반환값
zip()은 map()과 마찬가지로 실제 Python 리스트를 반환합니다.
예시
다음은 아래의 참조 구현을 바탕으로 한 몇 가지 예제입니다.:
>>> a = (1, 2, 3, 4)
>>> b = (5, 6, 7, 8)
>>> c = (9, 10, 11)
>>> d = (12, 13)
>>> zip(a, b)
[(1, 5), (2, 6), (3, 7), (4, 8)]
>>> zip(a, d)
[(1, 12), (2, 13)]
>>> zip(a, b, c, d)
[(1, 5, 9, 12), (2, 6, 10, 13)]
시퀀스들의 길이가 같으면 zip()을 되돌릴 수 있다는 점에 유의하십시오.:
>>> a = (1, 2, 3)
>>> b = (4, 5, 6)
>>> x = zip(a, b)
>>> y = zip(*x) # alternatively, apply(zip, x)
>>> z = zip(*y) # alternatively, apply(zip, y)
>>> x
[(1, 4), (2, 5), (3, 6)]
>>> y
[(1, 2, 3), (4, 5, 6)]
>>> z
[(1, 4), (2, 5), (3, 6)]
>>> x == z
1
시퀀스들의 길이가 모두 같지 않으면 이 방법으로 zip을 되돌릴 수 없습니다.
참조 구현
다음은 zip() 내장 함수를 Python으로 작성한 참조 구현입니다. 이 구현은 최종 승인 후 C 구현으로 대체될 것입니다.:
def zip(*args):
if not args:
raise TypeError('zip() expects one or more sequence arguments')
ret = []
i = 0
try:
while 1:
item = []
for s in args:
item.append(s[i])
ret.append(tuple(item))
i = i + 1
except IndexError:
return ret
BDFL 선언
참고: BDFL은 Python의 자비로운 종신 독재자(Benevolent Dictator For Life)인 Guido van Rossum을 가리킵니다.
- 함수의 이름. 이 PEP의 이전 버전에는
zip()을 대체할 이름 20여 개를 나열한 미해결 이슈가 포함되어 있었습니다. 압도적으로 더 나은 선택지가 없는 상황에서, BDFL은 Haskell [2]에서 이어받은 이름이라는 이유로zip()을 강하게 선호합니다. 대체 이름 목록은 이 PEP의 버전 1.7을 참고하십시오. zip()은 내장 함수여야 합니다.- 선택적 채우기. 이 PEP의 이전 버전에서는 인자 시퀀스들의 길이가 같지 않을 때 사용할 선택적
pad키워드 인자를 제안했습니다. 이는 사용자가 채우기 객체를 지정할 수 있다는 점을 제외하면map(None, ...)의 의미와 비슷한 동작입니다. 이 제안은 KISS 원칙에 따라 항상 가장 짧은 시퀀스에 맞춰 잘라 내는 방식이 채택되면서 BDFL에 의해 거부되었습니다. 정말로 필요하다면 나중에 추가하는 편이 더 쉽습니다. 필요하지 않은 기능이라도 한번 추가하면 앞으로 삭제하기는 여전히 불가능할 것입니다. - 지연 평가. 이 PEP의 이전 버전에서는
zip()이__getitem__()프로토콜을 사용해 지연 평가를 수행하는 내장 객체를 반환하도록 제안했습니다. 이 제안은 실제 Python 리스트를 반환하는 방식이 채택되면서 BDFL에 의해 강하게 거부되었습니다. 앞으로 지연 평가가 필요해진다면, BDFL은xzip()함수를 추가할 것을 제안합니다. - 인자가 없는
zip(). BDFL은 이 경우 TypeError 예외를 발생시키는 것을 강하게 선호합니다. - 인자가 하나인
zip(). BDFL은 이 경우 1-튜플의 리스트를 반환하는 것을 강하게 선호합니다. - 내부 및 외부 컨테이너 제어. 이 PEP의 이전 버전에는 일부 사람들이 원했던 기능, 즉 내부 및 외부 컨테이너 타입(이 버전의 PEP에서는 각각 튜플과 리스트입니다)을 제어하는 기능에 대한 상당히 긴 논의가 포함되어 있습니다. API와 구현이 단순해진 점을 고려하여 이 확장 제안은 거부되었습니다. 더 자세한 분석은 이 PEP의 버전 1.7을 참고하십시오.
이후의 zip() 변경 사항
Python 2.4에서 인자 없이 호출한 zip()은 TypeError 예외를 일으키는 대신 빈 리스트를 반환하도록 변경되었습니다. 원래 동작의 근거는 인자가 없다는 것이 프로그래밍 오류를 뜻한다고 여겨졌기 때문입니다. 그러나 그러한 생각은 가변 길이 인자 리스트를 언패킹하기 위해 zip()을 * 연산자와 함께 사용하는 경우를 예상하지 못했습니다. 예를 들어, zip의 역연산은 unzip = lambda s: zip(*s)와 같이 정의할 수 있습니다. 이 변환은 행렬 전치도 정의하며, 튜플의 리스트로 정의된 표에서는 이와 동등한 행/열 교환도 정의합니다. 후자의 변환은 레코드를 행으로, 필드를 열로 하는 데이터 파일을 읽을 때 흔히 사용됩니다. 예를 들어, 다음 코드는:
date, rain, high, low = zip(*csv.reader(file("weather.csv")))
열 형태의 데이터를 재배열하여 각 필드를 개별 튜플로 모으므로, 간단하게 순회하고 집계할 수 있습니다:
print "Total rainfall", sum(rain)
zip(*[])를 예외가 아닌 허용 가능한 경우로 처리하면 zip(*args)를 사용하는 코드를 더 쉽게 작성할 수 있습니다. 이는 레코드가 없는 빈 경우에서부터 데이터를 쌓아 올리거나, 그러한 빈 경우까지 재귀적으로 내려가는 경우에 특히 유용합니다.
이러한 가능성을 보고, BDFL은 (다소 우려하면서도) Py2.4에서 이 동작을 변경하는 데 동의했습니다.
기타 변경 사항
- 위에서 논의한
xzip()함수는 Py2.3에서itertools모듈에itertools.izip()으로 구현되었습니다. 이 함수는 지연(lazy) 동작을 제공하여, 매 단계마다 요소를 하나씩 소비하고 튜플 하나를 생성합니다. 이러한 “적시(just-in-time)” 방식은 리스트 기반의 대응물인zip()보다 메모리를 절약하고 더 빠르게 실행됩니다. itertools모듈에는itertools.repeat()와itertools.chain()도 추가되었습니다. 이 도구들을 함께 사용하면 (map(None, seqn)의 동작과 일치하도록) 시퀀스를None으로 채울 수 있습니다:zip(firstseq, chain(secondseq, repeat(None)))
참고 문헌
제안된 문법에 대해 Greg Wilson이 몇몇 컴퓨터 과학 대학원생을 대상으로 실시한 설문 조사 http://www.python.org/pipermail/python-dev/2000-July/013139.html
Copyright
This document has been placed in the public domain.