제너레이터는 제너레이터 이터레이터를 리턴하는 함수다(그래서 제너레이터 함수라고도 한다).
일반적인 함수랑 똑같이 생겼는데, 어떤 값을 return
하는 대신 yield
표현을 써서 for
루프나 next()
함수로 한 번에 하나씩 원소를 가져올 수 있는 값의 스트림을 반환하는 제너레이터 이터레이터/객체를 만들어준다.
아주 간단히 말하자면 yield
키워드를 함수에 썼다면 제너레이터 함수고, 제너레이터 이터레이터 객체를 리턴한다. (yield
는 제너레이터 함수를 만들때만 사용된다)
제너레이터라고 하면 일반적으로 제너레이터 함수를 의미하지만, 어떤 경우에는 제너레이터 이터레이터(제너레이터 함수에 의해 생성된 제너레이터 객체를 의미한다). 여기서는 명확히 하기 위해 제너레이터 함수와 제너레이터 이터레이터/객체라고 구분지어 말하겠다.
yield
가 들어간 함수를 돌리면 무슨 일이 일어나는데요?def generate_ints(N):
for i in range(N):
yield i
위는 아주 간단한 제너레이터 함수의 예다.
이 함수는 어떤 하나의 값을 리턴하지 않는다. 대신 yield
라는 애로 인해 이터레이터 프로토콜을 지원하는 제너레이터 객체를 리턴한다. (이터레이터와 이터러블에 대해서는 앞선 포스트에서 구체적으로 설명했으니 여기서는 아는 것을 전제로 하겠다.)
자, 이 함수를 호출했다고 해보자.
제너레이터 객체가 생성된 것을 볼 수 있다. 우리는 이 제너레이터 객체를 for
문에 쓸 수 있다. 우리가 이터러블이나 이터레이터를 for
문에 쓰듯!
for
루프 내부적으로는 무슨 일이 일어났을까? 이터레이터나 이터러블에 일어나는 과정과 같아 보인다!
구체적으로 어떤 일이 일어나는지 살펴보자. __next__()
메서드가 호출될 때마다, for
루프 내부적으로는 제너레이터 함수가 실행된다. 그리고 yield
오른쪽의 객체가 리턴되고, 제너레이터 함수의 실행 상태가 일시 중지되고, 지역 변수가 보존된다. 제너레이터의 __next__()
메서드가 실행되면 함수가 다시 yield
에 도달할 때까지 실행되고 또 다시 오른쪽 객체가 리턴되고 일시 중지된다. 루프는 제너레이터 함수가 더 이상 어떤 객체를 'yielding' 하지 않을 때까지 돌아가고 제너레이터 함수가 끝난다.
그냥 편하게 리스트를 만들면 될 것을 왜 굳이 제너레이터를 사용할까?
리스트를 하나 만들면 그 리스트의 모든 원소들이 메모리에 저장될 것이다. 여기서 두 가지 의문을 제기할 수 있겠다.
이런 문제를 해소하기 위해 우리는 리스트 대신 제너레이터를 쓸 수 있다. 제너레이터는 메모리에 객체를 '게으르게' 생성하고 로딩할 수 있게 해준다; 한 번에 하나씩 그리고 그런 객체를 얼마만큼이고 '제너레이트(생성)'해준다.
yield
를 써서 제너레이터 함수를 귀찮게 만들 바에는 그냥 리스트 쓰면 안되나요?yield
키워드를 써서 제너레이터 함수를 정의하는 대신 아주 간단하게 제너레이터를 만들 수 있다!
list comprehension 과 같은 방식으로 제너레이터 expression 을 만들 수 있다. 차이점은 대괄호를 소괄호로 만들어 줘야 한다는 것이다.
list_comprehension = [ i for i in range(5) ]
generator_expression = ( i for i in range(5) )
만드는 방법은 유사하지만, 결과물은 다르다. list comprehension 문이 모든 객체를 다 가지는(fully populated) 리스트를 만들어줬다면, generator expression은 제너레이터 객체를 리턴할 뿐이다. 그리고 순회할 때만 '게으르게' 원소를 하나씩 리턴해준다.
ge = (i for i in range(5))
generator expression 에 대해서 모르고 이 표현을 봤다면, 아마 tuple comprehension 이 아닌가 했을 것이다. 하지만 tuple comprehension 은 없다! (이걸 만들어야 한다는 논의가 있다고는 한다.)
튜플로 만들어주고 싶다면 아래처럼 하면된다.
>>> tuple((i for i in range(5))
(0, 1, 2, 3, 4)