소개 en ko

Python n-grouper

2013-02-28

파이썬 zip 함수 설명을 보면 다음과 같은 문장이 있다.

This makes possible an idiom for clustering a data series into n-length groups using zip(*[iter(s)]*n).

주어진 리스트 s를 순서대로 n개씩 묶어서 반환하는 것을 zip(*[iter(s)]*n) 를 통해 할 수 있다는 의미다. 이는 다음과 같이 사용할 수 있다.

s = [1, 2, 3, 4, 5, 6]
print zip(*[iter(s)]*2)   # [(1, 2), (3, 4), (5, 6)]

이 짧은 n-grouper 코드가 흥미있는 파이썬 문법을 함축적으로 보여줄 수 있어 소개한다.

iter 함수

주어진 객체의 멤버를 순회할 수 있는 iterator 객체를 만들어 반환한다. 만약 리스트가 넘겨졌다면 순서대로 내부 멤버를 반환하는 iterator 객체를 만들어 반환한다. 이런 경우 간단히 iterator 객체는 첫 번째 멤버를 가리키는 index 변수가지고 iterator 객체가 참조할 때마다 현재 index 가 가리키는 값을 반환하며 동시에 index 가 증가한다고 볼 수 있다.

s = [1, 2, 3, 4, 5, 6]
for i in iter(s):
  print i,   # 1 2 3 4 5 6

[…]*n

주어진 리스트를 n 번 반복해 붙여 반환한다. 만약 [1, 2, 3]*2 라면 [1, 2, 3, 1, 2, 3] 을 반환한다. 단 여기서 주의해야 할 점은 멤버가 반복될 때 얕은 복사가 (shallow copy) 수행된다는 점이다. 따라서 리스트 멤버가 객체 타입의 경우에는 반복되는 객체가 모두 동일한 객체임을 주의해야 한다.

a=[[1], [2]]*2
print a          # [[1], [2], [1], [2]]
a[0].append(3)
print a          # [[1, 3], [2], [1, 3], [2]]

이 경우 a 의 첫번째 값 뿐만 아니라 세번째 값도 변경되었음을 확인할 수 있다. 첫번째와 세번째가 같은 객체를 가리키기 때문이다. 따라서 [iter(s)]*2 로 나타내는 리스트 곱은 아래와 같이 수행된다. 여기서 zip 함수에 동일한 iterator 객체를 가리키는 i 가 두 개 넘겨졌음에 주의해야 한다.

i = iter(s)
print zip(*[i, i])

Unpacking argument *

파이썬의 함수 호출 문법의 재미있는 부분이다. *[…] 형태로 함수 인자를 넘겨주면 리스트 […] 의 멤버가 펼쳐져 함수 개별 인자로 넘겨진다. zip(*[i, i]) 는 아래와 같이 실행된다.

print zip(i, i)   # == zip(*[i, i])

함수 인자의 평가 순서

여기서는 당장 중요하지 않지만 파이썬의 함수 인자 평가는 왼쪽에서 오른쪽으로 정해져 있다. 따라서 아래와 같은 예제도 결과를 예측할 수 있다.

f=lambda x, y: x.append(y) or x
p=lambda x, y: x + y
a=[]
print p(f(a, 1), f(a, 2))   # [1, 2, 1, 2]

이는 C 언어의 경우 함수 인자의 평가 순서가 정해져 있지 않은 것과 비교된다. 아래의 예제는 컴파일러 혹은 옵션에 따라 서로 다른 결과를 반환하다.

int i=0;
p(i++, i++);

zip 함수의 실행 중 인자 평가 순서

zip 함수가 실행하면서 넘겨 받은 인자를 참조하는데 (iterator 이므로 next() 메소드가 불리는 방식으로) 이 순서를 zip 함수가 보장해 준다. 왼쪽에서 오른쪽으로 평가하는 것으로 되어 있는데 때문에 zip(i, j) 는 아래와 같이 수행됨을 보장한다.

i = iter(s)
j = i
print zip(i, j)
# zip(i, j):
#   r = []
#   while end:
#     a = i.next()
#     b = j.next()
#     r.append((a, b))
#   return r

이렇게 zip의 동작 방식을 정해둔 것은 같은 객체를 여러 인자를 통해 넘겼을 때에도 결과를 일정하게 보장하기 위함으로 보인다. 때문에 zip(i, i) 와 같은 코드의 수행 결과가 항상 일정할 수 있음을 기대할 수 있다.

결론

zip 함수로 구현한 n-grouper 는 파이썬의 여러 문법적 요소를 함축적으로 담고 있어 흥미로운 코드 임에는 틀림없다. 다만 객체가 얕은 복사가 발생하는 것에 주의하면서 동시에 zip 함수의 수행 방식을 숙지하고 있지 않으면 이해가 잘 안되는 코드가 좋은 코드인지는 잘 모르겠다. 개인적으로는 아래와 같이 다소 장황해 보이지만 의도가 명확한 코드가 더 좋지 않을까 싶다.

s = [1, 2, 3, 4, 5, 6]
n = 2
print [s[i-n:i] for i in range(n, len(s)+1, n)]
# [(1, 2), (3, 4), (5, 6)]