More Pythonic #3 - generator

Myungseo Kang bio photo By Myungseo Kang

이 글은 Pythonic 한 코드들을 개인적으로 학습하기 위한 포스팅이므로 두서없이 정리되었을 수 있습니다. 주의해주세요!

오늘은 Python 에서 iterator (반복자) 혹은 iterable (반복형) 한 객체를 더 잘 사용하기 위한 generator (제너레이터) 에 대해서 알아볼까 합니다.

generator (제너레이터)

Python 에는 generator (제너레이터) 라는 개념이 있는데, 이는 list 에 비해 메모리 공간을 효율적으로 사용합니다.

대충 뭔지는 알겠는데, 왜 효율적인지 어떻게 동작하는지에 대해 조금 더 깊이 알아보겠습니다.

generatorPython 문서에서 아래처럼 설명합니다.

Generators are a special class of functions that simplify the task of writing iterators. Regular functions compute a value and return it, but generators return an iterator that returns a stream of values.

영어가 나오니 잘 안 읽혀서 한글로 바꿔보면,

제너레이터는 iterator (반복자) 작성 작업을 단순하게 하는 특수한 함수의 클래스입니다. 일반적인 함수는 값을 계산하고 반환하지만, 제너레이터는 값 스트림을 반환하는 iterator (반복자) 를 반환합니다.

라고 하는데… 이해가 잘 되지 않으니, 아래에서 직접 사용해보면서 설명해보도록 하겠습니다.

yield 키워드

generatoryield 키워드를 통해 명시적으로 만들 수 있습니다.

>>> def gen_cats():
...     cats = ['Bengal', 'Scottish fold', 'Russian Blue']
...     for cat in cats:
...         yield cat
...
>>> 
>>> gen_cats()
<generator object gen_cats at 0x0150B5D8>
>>> [i for i in gen_cats()]
['Bengal', 'Scottish fold', 'Russian Blue']

이렇게 명시적으로 generator 를 반환하는 함수를 만들어 사용할 수도 있습니다.

generator expression (제너레이터 표현식)

generator expression (제너레이터 표현식, 이하 genexpr) 은 위에서 설명했던 generator 를 사용할 수 있는 방법 중 하나인데, 전 포스팅 (More Pythonic #1) 나왔던 listcomp 와 아주 흡사합니다.

listcomp 와 다른 점은 대괄호([]) 대신 소괄호(()) 를 사용한다는 점이 다릅니다.

아래 예시를 보면 이해가 더 쉽습니다.

>>> alphabets = ['A', 'B', 'C']
>>> listcomp = [i for i in alphabets]
>>> generator_expr = (i for i in alphabets)
>>> listcomp
['A', 'B', 'C']
>>> generator_expr
<generator object <genexpr> at 0x00CEB610>

이렇게 만든 변수들을 print 해보면 위처럼 listcomp 로 만든 list 는 값을 실제로 확인할 수 있는 반면, genexpr 를 사용해서 만든 변수는 그렇지 않습니다.

그러면 인덱스로 접근하면 값을 가져올 수 있을까요?

>>> generator_expr[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
>>> listcomp[0]
'A'

그것도 아니네요.

그럼 길이도 못 알아낼까요?

>>> len(generator_expr)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'generator' has no len()

정답입니다.

왜 이런걸까요?

도대체 왜 리스트보다 효율적인가요?

결론만 말하면, generator 는 정말 값을 반복시켜 가져오기 위한 iterator 를 만드는 데에 초점이 맞춰진 기능이기 때문입니다.

이 말을 풀어서 설명해보면,

generator 에 대한 설명 해석 중,

일반적인 함수는 값을 계산하고 반환하지만, 제너레이터는 값 스트림을 반환하는 iterator (반복자) 를 반환합니다.

라는 부분이 있는데, 이 부분 때문에 메모리를 효율적으로 사용하고 인덱스로는 접근할 수 없는 것이라고 할 수 있습니다.

제너레이터 함수를 호출하면 반환 값으로 단일 값 (위의 예시에선 list) 을 반환하지 않습니다.

yield 가 실행되면, return 키워드와 비슷하게 결과값을 반환합니다.

하지만 두 키워드의 가장 큰 차이점은 yield 의 경우, 실행되면 generator 의 실행 상태가 잠깐 중지되고 해당 함수의 지역 변수들이 보존된다는 점입니다.

그리고 다음에 해당 generator__next__() 메서드가 실행될 때, 위에서 잠깐 중지된 함수가 다시 실행되고 이것이 반복되는 것입니다.

따라서 메모리 상에 요소들에 미리 접근을 할 수 없습니다.

때문에 인덱스로 접근하거나 길이를 미리 알 수 없답니다.

그래서 iteratorgenerator 의 관계는,

generator 를 통해 iterator 를 만들어낼 수 있는 관계

정도로 생각할 수 있겠네요.

그래서 이렇게 메모리 공간을 효율적으로 이용하는 generator 의 경우에는 매우 많은 요소 (element) 를 다루거나 무한 스트림 (infinite stream) 을 다룰 때 아주 용이합니다.

다만 위에서 말한 인덱스로 접근하지 못하는 문제, 길이를 알 수 없는 문제는 고려해두면 좋겠네요.

다음에는 조금 더 새로운 주제로 찾아와볼게요. 읽어주셔서 감사합니다.