Python의 반복문

보통 for 구문을 사용해서 0부터 99까지 반복한다고 하면 다른 프로그래밍 언어에선 이런 느낌이 됩니다.

for (let i = 0; i < 99; ++i) {
  do_something(i);
}

하지만 Python에선 for문이 저런 구조가 아닙니다. 그렇기에 우리는 range를 쓴다고 알고 있습니다.

for i in range(100):
    do_something(i)

그럼, range는 뭘까요? range(100)을 실행하면 어떻게 되길래 우리가 원하는 반복문이 되버리는 걸까요? 일단 Python 2에서 확인해보도록 합시다.

>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> type(range(10))
<type 'list'>
>>> for i in range(10):
...   print i**3
...
0
1
8
27
64
125
216
343
512
729

100은 블로그에 담기엔 예시결과로는 너무 큰 관계로 10으로 줄였습니다. 실행하면 list로 0 이상 10 미만의 숫자가 들어가는 것을 확인할 수 있습니다. 도움말을 읽으면 당연히 알 수 있지만, range 함수는 정해진 범위를 list로 만들어줍니다. for문은 range의 결과로 나온 list에서 1개씩 뽑아서 사용하는 것이구요. Python의 for는 다른 언어에서의 foreach라고 부르는 것들과 비슷합니다.

for (let i of myArray) {
  do_something(i);
}

Python의 이런 반복은 얼핏보면 편하지만 함정이 있습니다. 만약 0부터 1000억까지 반복을 해야한다면 어떨까요? range(100000000)이라고 적으면 0부터 1000억-1 만큼의 인자가 들어있는 list가 생겨날 것입니다. list에 들어있는 것도 결국 컴퓨터가 저장하고 있어야 하는데 이대로 가다간 메모리가 버틸까요? 이 방법으로 접근하면 너무나도 비효율적입니다. Python으론 이런 방법밖에 없는걸까요?

이 문제를 해결하기 위해 Python 2에는 xrange라는 함수가 있습니다. Python 3에서는 기존의 rangexrange가 대체했습니다. 위의 예제들을 xrange로 바꿔서 다시 해보죠.

>>> xrange(10)
xrange(10)
>>> type(xrange(10))
<type 'xrange'>
>>> for i in xrange(10):
...   print i**3
...
0
1
8
27
64
125
216
343
512
729

일단 반복문이 잘 돌아간다는 것은 알 수 있지만 그 외의 값은 미궁속입니다.

Python 2의 xrange / Python 3 range 동작 살펴보기

일단 xrange가 어떻게 for문에 값을 전달하는지 알아봅시다. 먼저 xrange 값을 하나 만듭니다. 그리고 iter함수로 iterator를 받습니다.

>>> ten = xrange(10)
>>> it = iter(ten)
>>> it
<rangeiterator object at 0x10f27bde0>

그러면 이제 받은 값으로 ten의 구성요소에 하나씩 접속해볼 수 있습니다.

>>> next(it)
0
>>> next(it)
1
>>> next(it)
2

그리고 next 함수를 이용해서 값을 원하는 시점에 하나씩 받아올 수 있습니다. for문은 이 방식을 이용해서 xrange값의 iterator를 받고 내부적으로 next를 호출해서 사용합니다.

이것은 Python 3의 range도 동일합니다.

>>> ten = range(10)
>>> ten
range(0, 10)
>>> it = iter(ten)
>>> it
<range_iterator object at 0x106c70ed0>
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2

Generator

하지만 이건 Python에서 기본 지원하는 경우이고, 만약 수없이 많은 자료를 돌아다니면서 반복문을 돌려야한다면 어떨까요? 가령 수백만줄의 파일을 읽어서 한줄씩 처리중이었다면 어떻게 해야할까요? 모두 list에 올려놓고 하기엔 메모리가 버티지 못할 것입니다. 그 경우를 위해 Generator가 있습니다.

가볍게 0에서 9까지의 세제곱을 출력하는 Generator를 만들어서 사용해봅시다.

def gen():
    for i in range(10):
        yield i ** 3

for x in gen():
    print(x)

여기서 gen 함수가 바로 generator입니다.

>>> gen()
<generator object gen at 0x106aec1a8>

yield

Generator에서는 yield라는 키워드를 사용합니다. yield의 동작을 알아봅시다.

def gen():
    yield 'one'
    yield 'two'
    yield 'three'

g = gen()
print(next(g))  # one
print(next(g))  # two
print(next(g))  # three
print(next(g))  # raise StopIteration

yield는 함수 실행 중간에 빠져나올 수 있는 generator를 만들 때 사용합니다. return이었다면 'one'이 반환되고 끝났겠지만 실제로는 그 뒤로도 다시 사용할 수 있었죠.

yield는 단순히 값을 내보낼 수만 있는 것은 아니고, 넣어줄 수도 있습니다.

def gen():
    val = 111111
    while True:
        val = (yield val) * 111111

g = gen()
print(next(g))  # 111111
print(g.send(2))  # 222222
print(g.send(3))  # 333333

뭐에 쓰지?

위에서도 언급했지만 대용량 자료 처리등은 메모리에 모두 올려놓고 할 수 없습니다. 그런 경우 한 줄씩 읽은 뒤 generator를 이용한 반복처리를 하면 편합니다. 실제로도 Flask에서의 대용량 파일 전송, Sphinx의 확장 개발등에 사용됩니다.

요약

  1. yield 키워드를 사용하면 generator를 만들 수 있다.
  2. generator는 한번에 끝나지 않고 여러번에 걸쳐 입출력을 받을 수 있다.
  3. 대용량 처리 함수 제작 등에 편리하다
  4. 따라서, list가 필요한게 아니라면 Python 2에서 반복은 xrange를 쓰자.