Skip to content

28. 제너레이터(Generator)


1. 키워드

  • 제너레이터(Generator)


2. 제너레이터 사용하기

  • 제너레이터는 이터레이터를 생성해 주는 함수이다.
  • 이터레이터는 클래스에 __iter__, __next__ 또는 __getitem__ 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝이다.


3. 제너레이터와 yield 알아보기

  • 함수 안에서 yield를 사용하면 함수는 제너레이터가 되며 yield에는 값(변수)를 지정한다.


yield 


  • 이제 yield를 사용해서 제너레이터를 만들고 for 반복문에서 0, 1, 2 숫자 세 개를 출력해 보자.


def number_generator():
    yield 0
    yield 1
    yield 2

for i in number_generator():
    print(i)

# 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 예외가 발생한다.


>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
2
>>> g.__next__() # StopIteration


  • 이처럼 함수에 yield만 사용해서 간단하게 이터레이터를 구현할 수 있다.
  • 단, 이터레이터는 __next__ 메서드 안에서 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드의 반환값으로 나온다.
  • 또한, 이터레이터는 raiseStopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 자동으로 발생한다.
  • 제너레이터는 제너레이터 객체에서 __next__ 메서드를 호출할 때마다 함수 안의 yield까지 코드를 실행하며 yield에서 값을 발생시킨다.


2) for와 제너레이터

  • 다음과 같이 for 반복문은 반복할 때마다 __next__를 호출하므로 yield에서 발생시킨 값을 가져오고, StopIteration 예외가 발생하면 반복을 끝낸다.


001


  • 참고로 제너레이터 객체에서 __iter__를 호출하면 self를 반환하므로 같은 객체가 나온다.
  • 또한 yield는 양보하다라는 뜻으로, yield를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보한다.
  • 따라서 yield는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만든다.


3) yield의 동작 과정 알아보기

  • yield의 동작 과정을 알아보기 위해 for 반복문 대신 next 함수로 __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로 값을 가져온다.


  • nextyield의 동작 과정을 그림으로 살펴보자.
  • 먼저 g = number_generator()와 같이 제너레이터 객체를 만든다.
  • 그다음에 next(g)를 호출하면 제너레이터 안의 yield 0이 실행되어 숫자 0을 전달한 뒤 바깥의 코드가 실행되도록 양보한다.
  • 함수 바깥에서는 print(a)next(g)에서 반환된 값을 출력한다.


002


  • 값을 출력했으면 next(g)로 다시 제너레이터 안의 코드를 실행한다.
  • 이때는 yield 1이 실행되고 숫자 1을 발생시켜서 바깥으로 전달한다.
  • 그리고 함수 바깥에서는 print(b)next(g)에서 반환된 값을 출력한다.


003


  • 마찬가지 과정으로 yield 2도 숫자를 발생시키고 print(c)로 제너레이터에서 나온 값을 출력한다.


004


  • 이렇게 제너레이터는 함수를 끝내지 않은 상태에서 yield를 사용하여 값을 바깥으로 전달할 수 있다.
  • 즉, return은 반환 즉시 함수가 끝나지만 yield는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식이다.


제너레이터와 return

  • 제너레이터는 함수 끝까지 도달하면 StopIteration 예외가 발생한다.
  • 마찬가지로 return도 함수를 끝내므로 return을 사용해서 함수 중간에 빠져나오면 StopIteration 예외가 발생한다.
  • 특히 제너레이터 안에서 return에 반환값을 지정하면 StopIteration 예외의 에러 메시지로 들어간다.


def one_generator():
    yield 1

    return "return에 지정한 값"

try:
    g = one_generator()
    next(g)
    next(g)
except StopIteration as e:
    print(e) # return에 지정한 값


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으로 숫자를 바깥으로 전달한 뒤 n1 증가시키면 된다.
  • 여기서는 yield3번 나오므로 for 반복문도 3번 반복한다.


  • 물론 다음과 같이 next 함수(__next__ 메서드)도 3번 사용할 수 있다.


>>> g = number_generator(3)

>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g) # StopIteration


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 반복가능한객체
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__ 메서드)를 세 번 호출할 수 있다.


>>> g = number_generator()

>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g) # StopIteration


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()를 사용하면 숫자를 세 번 출력한다.


제너레이터 표현식

  • 리스트 표현식을 사용할 때 [](대괄호)를 사용했다.
  • 같은 리스트 표현식을 ()(괄호)로 묶으면 제너레이터 표현식이 된다.
  • 리스트 표현식은 처음부터 리스트의 요소를 만들어내지만 제너레이터 표현식은 필요할 때 요소를 만들어내므로 메모리를 절약할 수 있다.


( for 변수 in 반복가능한객체)
>>> [i for i in range(50) if i % 2 == 0]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48]

>>> (i for i in range(50) if i % 2 == 0)
<generator object <genexpr> at ...>

References