오늘 학습할 부분
iterator, generator
이터레이터(iterator)는 값을 차례대로 꺼낼 수 있는 객체입니다. 지금까지 for 반복문을 사용할 때 range를 사용했습니다. 만약 100번 반복하고 싶다면,
for i in range(100):
으로 만듭니다. 이때 이터레이터가 0부터 99까지의 값을 차례대로 값을 만드는 역할을 합니다.
연속된 숫자를 만들 때 아주 많을 때는 메모리를 많이 사용하게 되므로 성능에 불리합니다. 그래서 파이썬에서는 이터레이터만 생성하고 값이 필요한 시점이 되었을 때 값을 만드는 방식을 사용합니다.
즉, 데이터 생성을 뒤로 미루는 건데 이런 방식을 지연 평가(lazy evaluation)이라 합니다. 이터레이터는 반복자라 부르기도 합니다.
반복 가능한 객체(iterable)는 말 그대로 반복할 수 있는 객체인데 우리가 흔히 사용하는 문자열, 리스트, 딕셔너리, 세트가 반복 가능한 객체입니다. 요소가 여러 개 들어있고, 한 번에 하나씩 꺼낼 수 있는 객체를 뜻합니다.
객체가 반복 가능한 객체인지 확인하는 방법은 객체에 __iter__ 메서드가 있는지 확인해보면 됩니다. 다음과 같이 dir 함수를 사용하면 객체의 메서드를 확인할 수 있습니다.
dir([1, 2, 3])
리스트 [1, 2, 3]을 dir로 살펴보면 __iter__ 메서드가 들어있습니다. 이 리스트에서 __iter__을 호출하면 이터레이터가 나옵니다.
[1, 2, 3].__iter__()
>>> <list_iterator object at 0x03616630>
리스트의 이터레이터를 변수에 저장한 뒤 __next__ 메서드를 호출하면 요소를 차례대로 꺼낼 수 있습니다.
>>> it = [1, 2, 3].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<pyshell#48>", line 1, in <module>
it.__next__()
StopIteration
it에서 __next__를 호출할 때마다 리스트 안에 들어 있는 1, 2, 3이 나옵니다. 3 다음에 __next__를 호출하면 StopIteration 예외가 발생합니다. 즉, [1, 2, 3] 이므로 1, 2, 3 세 번 반복합니다.
이처럼 이터레이터는 __next__로 요소를 계속 꺼내다가 꺼낼 요소가 없으면 StopIteration 예외를 발생시켜 반복을 끝냅니다.
여기서 리스트 뿐만 아니라 문자열, 딕셔너리, 세트도 __iter__를 호출하면 이터레이터가 나오고, 이터레이터에서 __next__를 호출하면 차례대로 값을 꺼냅니다.
range에서 __iter__로 이터레이터를 얻어낸 뒤 __next__ 메서드를 호출해봅니다.
>>> it = range(3).__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
it.__next__()
StopIteration
for에 반복 가능한 객체를 사용했을 때 동작 과정을 보겠습니다.
for에 range(3)을 사용했다면 먼저 range에서 __iter__로 이터레이터를 얻습니다. 그리고 한 번 반복될 때마다 이터레이터에서 __next__로 숫자를 꺼내 i에 저장하고, 지정된 숫자 3이 되면 StopIteration을 발생시켜 반복을 꺼냅니다.
이처럼 반복 가능한 객체는 __iter__ 메서드로 이터레이터를 얻고, 이터레이터의 __next__ 메서드로 반복합니다. 여기서는 반복 가능한 객체와 이터레이터가 분리되어 있지만, 클래스에 __iter__, __next__ 를 모두 구현하면 이터레이터를 만들 수 있습니다. 특히 __iter__, __next__ 를 가진 객체를 이터레이터 프로토콜(iterator protocol)을 지원합니다.
제너레이터는 이터레이터를 생성해주는 함수입니다. 이터레이터는 __iter__, __next__ 또는 __getitem__ 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝입니다. 그래서 제너레이터는 이터레이터보다 훨씬 간단하게 작성할 수 있습니다.
제너레이터는 발생자라고 부르기도 합니다!
def number_generator():
yield 0
yield 1
yield 2
for i in number_generator():
print(i)
>>> 0
1
2
for 반복문에 number_generator()를 지정해서 값을 출력해보면 yield에 지정했던 0, 1, 2가 나옵니다.
number_generator 함수로 만든 객체가 정말 이터레이터인지 살펴보겠습니다.
g = number_generator()
>>> g
<generator object number_generator at 0x03A190F0>
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
numbergenerator 함수를 호출하면 제너레이터 객체(generator object가 반환됩니다. 이 객체를 dir 함수로 살펴보면 이터레이터에서 볼 수 있는 __iter__, __next__ 메서드가 들어있습니다.
제너레이터의 __next__를 호출해보겠습니다.
>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
2
>>> g.__next__()
Traceback (most recent call last):
File "<pyshell#29>", line 1, in <module>
g.__next__()
StopIteration
이터레이터와 동작이 똑같이 작동합니다. 함수에 yield만 사용해서 간단하게 이터레이터를 구현할 수 있습니다. 단, 이터레이터는 __next__ 메서드 안에 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드의 반환값으로 나옵니다. 또한, 이터레이터는 raise로 StopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 자동으로 발생합니다.
제너레ㅣ터는 제너레이터 객체에서 __next__ 메서드를 호출할 떄마다 yield 까지 코드를 실행하며 값을 발생시킵니다.
for 반복문은 반복할 때마다 __next__를 호출하므로 yield에서 발생시킨 값을 가져옵니다. 그리고 StopIteration 예외가 발생하면 반복을 끝냅니다.
yield를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보합니다. 따라서 yield는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만듭니다.
def number_generator():
yield 0 # 0을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
yield 1 # 1을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
yield 2 # 2를 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
g = number_generator()
a = next(g) # yield를 사용하여 함수 바깥으로 전달한 값은 next의 반환값으로 나옴
print(a) # 0
b = next(g)
print(b) # 1
c = next(g)
print(c) # 2
>>> 0
1
2
값을 출력했으면 next(g)로 다시 제너레이터 안의 코드를 실행합니다. 이때는 yield 1이 실행되고 숫자 1을 발생시켜서 바깥으로 전달합니다. 그리고 함수 바깥에서는 print(b)로 next(g)에서 반환된 값을 출력합니다.
이렇게 제너레이터는 함수를 끝내지 않은 상태에서 yield를 사용하여 값을 바깥으로 전달할 수 있습니다. 즉, return은 반환 즉시 함수가 끝나지만 yield는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식입니다.