제너레이터
파이썬의 제너레이터(Generator)는 한 번에 하나씩 구성 요소를 반환해주는 이터러블(iterable)을 생성해 주는 객체입니다. 데이터를 모두 메모리에 저장하는 대신 특정 요소를 만들 줄 아는 객체를 만들어 필요할 때마다 하나씩 가져옵니다. 따라서 제너레이터를 사용하면 메모리의 낭비를 막을 수 있습니다.
여러 예제를 통해서 알아보겠습니다.
예제 1

파이썬 내장 함수인next()
는 이터러블을 다음 요소로 이동시키고 기존 값을 반환합니다. 그리고 이터레이터가 더 이상의 값을 가지고 있지 않다면 StopIteration 예외가 발생합니다. 이 예외는 반복이 끝났다는 것을 나타내며 사용할 수 있는 요소가 없음을 나타냅니다.
예제 2

enumerate()
함수와 비슷한 기능을 할 수 있는 객체를 만들어 볼겁니다. 그러기 위해서는 무한 시퀀스를 만들어야 합니다. 그리고 이터레이터 객체여야 합니다.

__next__()
매직 메서드와 __iter__()
매직 메서드를 구현하면 이터레이터 객체가 됩니다. 이 객체는 반복이 가능하며 next()
내장 함수도 사용 가능합니다.

제너레이터를 사용하면 훨씬 간단하면서 똑같은 역할을 하는 객체를 만들 수 있습니다. 클래스를 만드는 대신 필요한 값을 yield
하는 함수를 만들면 됩니다.

함수의 형태이지만 yield
키워드가 해당 함수를 제너레이터로 만들어 줍니다.
제너레이터 함수가 호출되면 yield
문장을 만나기 전 까지 실행되며 yield
문장을 만나면 그 값을 반환하고 그 자리에서 멈춥니다. 따라서 무한 루프를 사용해도 안전합니다.
예제3
2차원 이상의 반복을 통해 값을 찾아야 할 때 가장 단순한 방법으로는 중첩 루프를 이용한 탐색이 있습니다. 값을 찾으면 break
를 해야하는데 중첩 루프이므로 두 번 break
를 호출해야 하는 상황입니다. 예외나 플래그를 사용하는건 좋지 못한 방법입니다.
가장 좋은 방법은 중첩 루프를 없애는 것입니다.

다음은 제너레이터를 사용하여 중첩루프를 없애고 반복을 추상화 한 코드입니다.

generator_arr_2d()
는 2차원이상의 array
를 파라미터로 받아 위치와 그에 해당하는 cell 을 반환하는 제너레이터 입니다. 중첩루프를 탐색하는 함수인 search_good()
함수에서는 2중 루프를 사용하지 않으며 제너레이터 표현식만을 사용합니다. 지금은 2차원 배열을 사용했지만 나중에 더 높은 차원의 배열을 사용할지라도 클라이언트는 그것에 대해 알 필요 없이 기존 코드를 그대로 사용하면 됩니다.
이터러블과 이터레이터
이터러블과 이터레이터는 비슷해 보이지만 서로 다른 개념입니다. 이터러블은 for ... in ...
루프를 아무 문제 없이 실행할 수 있다는 것을 뜻합니다. 이터레이터는 단지 내장 next()
함수 호출 시 한 번에 하나씩 값을 생성하는 객체입니다. 즉 이터레이터를 호출하지 않은 상태에서 다음 값을 요청 받기 전까지는 얼어있는 상태이고, 이런 의미에서 모든 제너레이터는 이터레이터 입니다.
다음은 이터러블하지 않은 이터레이터 객체의 예시 입니다. 오직 한 번에 하나만 값을 반환합니다.

이러한 에러가 발생하는 이유는 __iter__()
메서드를 구현하지 않았기 때문입니다.

__iter__()
메서드를 구현하면 위와 같이 for ... in ...
구문에 사용해도 에러가 발생하지 않습니다.
코루틴
제너레이터는 반복 가능한 객체로 __iter__()
와 __next__()
를 구현합니다. 이러한 프로토콜은 파이썬에 의해 자동 제공되므로 제너레이터 객체는 next()
함수를 이용해서 반복 또는 다음 요소로의 이동이 가능합니다.
또한 제너레이터는 코루틴으로도 활용할 수 있습니다. 이를 위해 추가된 메서드가 총 3개 있는데 바로 close()
throw()
send()
입니다.
close()
이 메서드를 호출하면 제너레이터에서 GeneratorExit 예외가 발생합니다. 예외를 따로 처리하지 않으면 반복이 중지되며 이 메서드는 종료상태를 지정하는데 사용할 수 있습니다.
throw()
이 메서드는 현재 제너레이터가 중단된 위치에서 예외를 발생시킵니다. 제너레이터가 예외를 처리했으면 해당 except
절에 있는 코드가 호출됩니다. 예외 처리를 하지 않았다면 예외가 호출자에게 전파되고 제너레이터는 중지됩니다.
send()
next()
는 제너레이터에 파라미터를 전달할 수 없지만 send()
를 사용하면 파라미터 전달이 가능합니다. 간단한 예시와 함께 알아보겠습니다.

send()
메서드를 사용했다는 것은 yield
키워드가 할당 구문의 오른쪽( addition = yield num
)에 있다는 것이고 인자 값을 받아 다른 곳에 할당할 수 있음을 뜻합니다.
코루틴에서는 일반적으로 다음과 같은 폼의 yield
키워드를 사용합니다.receive = yield produced
이 경우 yield
키워드의 기능은 produced 값을 호출자에게 보내고 그곳에 멈추는 것(호출자가 next()
메서드를 사용해 값을 가져오는 것) 과 호출자로부터 send()
를 통해 전달된 produced 값을 받는 것, 두 가지 입니다.
send()
메서드를 사용하려면 next()
메서드를 적어도 한 번은 써줘야 에러가 발생하지 않습니다.
물론 이를 간단하게 해결하는 방법이 있습니다. 다음 예제 코드를 통해 알아보겠습니다.

prepare_coroutine
이라는 데코레이터를 사용해서 next()
를 쓰지 않아도 send()
메서드 이용이 가능하게 했습니다.
test_send
제너레이터의 코드도 보다 깔끔하게 바꿨습니다.addition = yield num
if addition is None or addition is 0:
addition = pre_addition
이 세 줄의 코드를addition = (yield num) or addition
한 줄로 바꿔서 표현할 수 있습니다.
yield from
제너레이터는 파이썬에서 def
키워드를 이용한 함수처럼 표현되지만 일반 함수가 아니므로 a = generator()
라고 하면 제너레이터 객체를 생성할 뿐이지 값을 반환하진 않습니다. 반복을 해야 값을 가져올 수 있는 것입니다.
이를 해결하기 위해 제너레이터에서 return
을 사용하면 값을 반환하는 즉시 StopIteration 예외가 발생하며 더 이상 반복을 할 수 없게 됩니다.

이 점을 개선하기 위한 구문이 바로 yield from
입니다.
간단한 예시와 함께 살펴보겠습니다.

이는 yield from
구문을 사용하면 중첩 루프를 피할 수 있습니다.

위 두 코드는 같은 역할을 수행합니다.
yield from
은 어떠한 이터러블에 대해서도 동작하며 최상위 제너레이터가 직접 값을 yield
한 것과 같은 효과를 나타냅니다.
참고자료
Mariano Anaya, 『파이썬 클린코드』, 김창수, 터닝포인트(2019), p.206 -p.231