28. 제너레이터(Generator)
1. 키워드
- 제너레이터(Generator)
2. 제너레이터 사용하기
- 제너레이터는 이터레이터를 생성해 주는 함수이다.
- 이터레이터는 클래스에
__iter__
,__next__
또는__getitem__
메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝이다.
3. 제너레이터와 yield
알아보기
- 함수 안에서
yield
를 사용하면 함수는 제너레이터가 되며yield
에는 값(변수)를 지정한다.
- 이제
yield
를 사용해서 제너레이터를 만들고for
반복문에서0
,1
,2
숫자 세 개를 출력해 보자.
for
반복문에number_generator()
를 지정해서 값을 출력해 보면yield
에 지정했던0
,1
,2
가 나오며, 이는 이터레이터와 사용 방법이 똑같다.
1) 제너레이터 객체가 이터레이터인지 확인하기
- 그럼
number_generator
함수로 만든 객체가 정말 이터레이터인지 살펴보자.
- 다음과 같이
dir
함수로 메서드 목록을 확인해 보자.
>>> g = number_generator()
>>> g
<generator object number_generator at ...>
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
number_generator
함수를 호출하면 제너레이터 객체가 반환된다.- 이 객체를
dir
함수로 살펴보면 이터레이터에서 볼 수 있는__iter__
,__next__
메서드가 들어있다.
- 실제로 제너레이터 객체의
__next__
를 호출해 보면 숫자0
,1
,2
가 나오다가StopIteration
예외가 발생한다.
- 이처럼 함수에
yield
만 사용해서 간단하게 이터레이터를 구현할 수 있다. - 단, 이터레이터는
__next__
메서드 안에서 직접return
으로 값을 반환했지만 제너레이터는yield
에 지정한 값이__next__
메서드의 반환값으로 나온다. - 또한, 이터레이터는
raise
로StopIteration
예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면StopIteration
예외가 자동으로 발생한다. - 제너레이터는 제너레이터 객체에서
__next__
메서드를 호출할 때마다 함수 안의yield
까지 코드를 실행하며yield
에서 값을 발생시킨다.
2) for
와 제너레이터
- 다음과 같이
for
반복문은 반복할 때마다__next__
를 호출하므로yield
에서 발생시킨 값을 가져오고,StopIteration
예외가 발생하면 반복을 끝낸다.
- 참고로 제너레이터 객체에서
__iter__
를 호출하면self
를 반환하므로 같은 객체가 나온다. - 또한
yield
는 양보하다라는 뜻으로,yield
를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보한다. - 따라서
yield
는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만든다.
3) yield
의 동작 과정 알아보기
yield
의 동작 과정을 알아보기 위해for
반복문 대신next
함수로__next__
메서드를 직접 호출해 보자.
def number_generator():
yield 0
yield 1
yield 2
g = number_generator()
a = next(g)
print(a)
b = next(g)
print(b)
c = next(g)
print(c)
# 0
# 1
# 2
yield
를 사용하여 바깥으로 전달한 값은next
함수(__next__
메서드)의 반환값으로 나온다.- 따라서
next(g)
의 반환값을 출력해 보면yield
에 지정한 값0
,1
,2
가 차례대로 나온다. - 즉, 제너레이터 함수가 실행되는 중간에
next
로 값을 가져온다.
next
와yield
의 동작 과정을 그림으로 살펴보자.- 먼저
g = number_generator()
와 같이 제너레이터 객체를 만든다. - 그다음에
next(g)
를 호출하면 제너레이터 안의yield 0
이 실행되어 숫자0
을 전달한 뒤 바깥의 코드가 실행되도록 양보한다. - 함수 바깥에서는
print(a)
로next(g)
에서 반환된 값을 출력한다.
- 값을 출력했으면
next(g)
로 다시 제너레이터 안의 코드를 실행한다. - 이때는
yield 1
이 실행되고 숫자1
을 발생시켜서 바깥으로 전달한다. - 그리고 함수 바깥에서는
print(b)
로next(g)
에서 반환된 값을 출력한다.
- 마찬가지 과정으로
yield 2
도 숫자를 발생시키고print(c)
로 제너레이터에서 나온 값을 출력한다.
- 이렇게 제너레이터는 함수를 끝내지 않은 상태에서
yield
를 사용하여 값을 바깥으로 전달할 수 있다. - 즉,
return
은 반환 즉시 함수가 끝나지만yield
는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식이다.
제너레이터와 return
- 제너레이터는 함수 끝까지 도달하면
StopIteration
예외가 발생한다. - 마찬가지로
return
도 함수를 끝내므로return
을 사용해서 함수 중간에 빠져나오면StopIteration
예외가 발생한다. - 특히 제너레이터 안에서
return
에 반환값을 지정하면StopIteration
예외의 에러 메시지로 들어간다.
4. 제너레이터 만들기
- 이번에는
range(횟수)
처럼 동작을 하는 제너레이터를 만들어보자.
def number_generator(stop):
n = 0
while n < stop:
yield n
n += 1
for i in number_generator(3):
print(i)
# 0
# 1
# 2
- 제너레이터 안에서 변수
n
을 만들고0
을 저장한다. - 그리고
while n < stop:
과 같이 반복을 끝낼 숫자보다 작을 때 반복하도록 만든다. - 반복문 안에서는
yield n
으로 숫자를 바깥으로 전달한 뒤n
을1
증가시키면 된다. - 여기서는
yield
가3
번 나오므로for
반복문도3
번 반복한다.
- 물론 다음과 같이
next
함수(__next__
메서드)도3
번 사용할 수 있다.
1) yield
에서 함수 호출하기
- 다음은 리스트에 들어있는 문자열을 대문자로 변환하여 함수 바깥으로 전달한다.
def upper_generator(x):
for i in x:
yield i.upper()
fruits = ["apple", "pear", "grape", "pineapple", "orange"]
for i in upper_generator(fruits):
print(i)
# APPLE
# PEAR
# GRAPE
# PINEAPPLE
# ORANGE
- 리스트
fruits
에 들어있는 문자열이 모두 대문자로 출력되었다. yield i.upper()
와 같이yield
에서 함수(메서드)를 호출하면 해당 함수의 반환값을 바깥으로 전달한다.upper
는 호출했을 때 대문자로 된 문자열을 반환하므로yield
는 이 문자열을 바깥으로 전달한다.- 즉,
yield
에 무엇을 지정하든 결과만 바깥으로 전달한다.
5. yield from
으로 값을 여러 번 바깥으로 전달하기
- 지금까지
yield
로 값을 한 번씩 바깥으로 전달했다. - 그래서 값을 여러 번 바깥으로 전달할 때는
for
또는while
반복문으로 반복하면서yield
를 사용했다.
- 다음은 리스트의
1
,2
,3
을 바깥으로 전달한다.
def number_generator():
x = [1, 2, 3]
for i in x:
yield i
for i in number_generator():
print(i)
# 1
# 2
# 3
- 이런 경우에는 매번 반복문을 사용하지 않고,
yield from
을 사용하면 된다. yield from
에는 반복 가능한 객체, 이터레이터, 제너레이터 객체를 지정한다.
- 그럼
yield from
에 리스트를 지정해서 숫자1
,2
,3
을 바깥으로 전달해 보자.
def number_generator():
x = [1, 2, 3]
yield from x
for i in number_generator():
print(i)
# 1
# 2
# 3
yield from x
와 같이yield from
에 리스트(반복 가능한 객체)를 지정했다.- 이렇게 하면 리스트에 들어있는 요소를 한 개씩 바깥으로 전달한다.
- 즉,
yield from
을 한 번 사용하여 값을 세 번 바깥으로 전달한다. - 따라서
next
함수(__next__
메서드)를 세 번 호출할 수 있다.
1) yield from
에 제너레이터 객체 지정하기
- 이번에는
yield from
에 제너레이터 객체를 지정해 보자.
def number_generator(stop):
n = 0
while n < stop:
yield n
n += 1
def three_generator():
yield from number_generator(3)
for i in three_generator():
print(i)
# 0
# 1
# 2
- 먼저 제너레이터
number_generator
는 매개변수로 받은 숫자 직전까지 숫자를 만들어낸다. - 그리고
three_generator
에서는yield from number_generator(3)
과 같이yield from
에 제너레이터 객체를 지정했다. number_generator(3)
은 숫자를 세 개를 만들어내므로yield from number_generator(3)
은 숫자를 세 번 바깥으로 전달한다.- 따라서
for
반복문에three_generator()
를 사용하면 숫자를 세 번 출력한다.
제너레이터 표현식
- 리스트 표현식을 사용할 때
[]
(대괄호)를 사용했다. - 같은 리스트 표현식을
()
(괄호)로 묶으면 제너레이터 표현식이 된다. - 리스트 표현식은 처음부터 리스트의 요소를 만들어내지만 제너레이터 표현식은 필요할 때 요소를 만들어내므로 메모리를 절약할 수 있다.