Python3의 Generator 알아보기

제너레이터

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

여러 예제를 통해서 알아보겠습니다.


예제 1

제너레이터의 간단한 예시 및 실행

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


예제 2

Python 내장 함수인 enumerate()

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__() 메서드를 구현한 이터레이터

__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 예외가 발생하며 더 이상 반복을 할 수 없게 됩니다.

return 했더니 10001 까지 가지 않는 제너레이터

이 점을 개선하기 위한 구문이 바로 yield from 입니다.

간단한 예시와 함께 살펴보겠습니다.

여러 이터러블을 받아 하나의 스트림으로 반환하는 제너레이터

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

중첩 루프가 사라졌다.

위 두 코드는 같은 역할을 수행합니다.

yield from 은 어떠한 이터러블에 대해서도 동작하며 최상위 제너레이터가 직접 값을 yield한 것과 같은 효과를 나타냅니다.


참고자료

Mariano Anaya, 『파이썬 클린코드』, 김창수, 터닝포인트(2019), p.206 -p.231

You've successfully subscribed to Digitalize offline space with AI
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.