Python3의 Generator 알아보기

2 months ago   •   9 min read

By mAsh

제너레이터

파이썬의 제너레이터(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

Spread the word

Keep reading