이터러블을 구현하다 보면 실제 기능은 대부분 이터레이터에서 구현하고 이터러블 객체는 단순히 이터레이터를 초기화 하기위해 필요한 값만 전달해 줍니다. 클래스를 하나 더 만드는 것이 어려운 일은 아니지만 코드가 늘어납니다. 또한 하나의 기능을 두 개의 클래스로 구현하는 것은 직관적이지 않습니다. 파이썬은 이를 해결할 수 있는 방법을 제공합니다.

1. 제너레이터 ( Generator )

“제너레이터는 yield 키워드가 들어간 함수다”라고 써있는 글을 봤지만, 맞는지 모르겠습니다. 이 함수는 호출시 내부의 코드가 실행되는 것이 아니라 조금 특별한 객체를 반환합니다. 코드를 작성해 타입을 알아보니 제너레이터 객체라고 뜹니다. 그러므로 이 글에서는 yield 키워드가 들어간 함수를 제너레이터 함수라고 하고, 이 함수에서 만들어진 객체를 제너레이터 객체라고 하겠습니다. 중요한건 제너레이터 함수는 제너이터 객체를 반환하고, 이 제너레이터 객체는 이러테리터이며, 순환방법은 제너레이터 함수에 의해 결정된다는 사실입니다.

제너레이터 객체의 __next__ 메소드가 호출되면 자신과 관련된 제너레이터 함수를 실행합니다. yield 키워드를 발견하면 진행상태를 저장하고 잠시 멈춥니다. __next__ 메소드는 수확된 값(yield 뒤쪽의 값)을 반환합니다. 그 다시 __next__ 메소드가 호출되면 저장된 진행상태를 불러오고 멈춘 지점부터 이어갑니다. 또 다시 yield 키워드에서 멈추고 수확된 값을 반환합니다. 이렇게 제너레이터 함수가 끝날 때까지 반복됩니다. 함수가 끝나면 next 함수는 StopIteration를 일으킵니다.

간단한 사용예는 다음과 같습니다.

code =

1
2
3
4
5
6
7
8
def mygenerator():
    yield 1
    yield 'hello'
    yield 3.5
        
g = mygenerator()
for i in g:
    print(i)

result =

1
2
3
1
'hello'
3.5

이터러블을 구현하는 것보다 간단합니다. 또한 이터러블을 구현할 시, 순환이 끝난 이터레이터가 StopIteration을 반환하지 않거나, 이터레이터의 __next__ 메소드가 자신을 반환하지 않게 되는 실수를 범하지 않아도 됩니다. 또한 제너레이터는 이터레이터와 달리 함수처럼 전달값을 받을 수 있습니다. 이를 이용해 한 객체를 다양한 방법으로 순환할 수 있습니다.

2. 제너레이터 표현법 ( Generator Expression )

간단한 함수를 정의할 때 여러줄에 걸쳐 쓰는 대신 한줄표현인 람다를 사용합니다. 제너레이터도 비슷한 기능이 있습니다. 제너레이터 표현법(generator expression)입니다. for 문과 if 문이 서로 겹겹이 싸여있는 구조를 생각해봅시다. 단, 가장 바깥쪽에는 for 문이 옵니다. 그리고 가장 안쪽에서 하나의 문장(statement)이 실행됩니다. 아래 예시로 mygenerator 함수가 있습니다. 이러한 구조는 한줄 표현이 가능합니다. 우선 괄호를 열고 가장 안쪽에서 실행되는 문장을 입력합니다. 그리고 겹겹이 싸인 구조를 콜론 없이 줄바꿈 없이 입력합니다. 아래 예시에서 g1g2 같은 값을 순환합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# iterable1, iterable2 : 임의의 이터러블
# condition1, condition2 : 임의의 조건문
# do_something: 임의의 함수
def mygenerator():
	for i in iterable1:
		if condition1(i):
			for k in iterable2:
				if condition2(k):
					yield do_something(k)
					
g1 = def mygenerator()
g2 = ( do_something(k) for i in iterable1 if condition1(i) for k in iterable2 if condition2(k) )

또한 제너레이터 표현법은 mapfilter를 대체할 수 있습니다. product 또한 부분적으로 대채할 수 있지만, 다중 반복문의 안쪽 반복문에 이터레이터를 사용할 수 없기 때문에 product를 쓰는 것이 낫습니다. 의미전달 면에서도 product 가 명확합니다. 아래 예시에서 g1과 g2, g3와 g4, g5와 g6은 서로 같은 값을 순환합니다. 그리고 g6, g7, g8은 서로 같은 값을 순환합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# iterable : 임의의 이터러블 
# iter1, iter2, iter3 : 임의의 이터레이터가 아닌 이터러블
# condition : 임의의 조건문
# do_something : 임의의 함수
g1 = map(lambda x: do_something(x), iterable)
g2 = (do_something(x) for x in iterable)

g3 = filter(lambda x: condition(x), iterable)
g4 = (x for x in iterable if condition(x))

g5 = map(lambda x: do_something(x), filter(lambda x: condition(x), iterable))
g6 = (do_something(x) for x in iterable if condition(x))

from itertools import product
g6 = map(lambda x, y, z: do_something(x, y, z), filter(lambda x, y, z: condition(x, y, z), product(iter1, iter2, iter3)))
g7 = (do_something(x,y,z) for x in iter1 for y in iter2 for z in iter3 if condition(x,y,z))
g8 = (do_something(x,y,z) for x, y, z in product(iter1, iter2, iter3) if condition(x,y,z))

3. 마치며

제너레이터 객체도 이터레이터입니다. __next__ 메소드를 가지며, __iter__는 스스로를 반환합니다. 제너레이터가 어떤 객체의 메소드일 경우 제너레이터, 객체와 제너레이터의 관계는 이터러블과 이러레이터의 관계입니다. 단 하나의 객체가 여러개의 제너레이터를 메소드로 가질수 있고 전달값 마음데로 정할 수 있기에 제너레이터가 더 유연합니다. 제너레이터가 메소드가 아닌 함수라면 enumeratemap, filter 처럼 객체를 순환하는 도구로 사용할 수 있습니다.


A. 제너레이터를 이용한 코딩

code = 더해서 10이 되는 임의의 두 자연수 찾기

1
print(*( (i,j) for i in range(10) for j in range(10) if i + j == 10))

result =

1
(1, 9), (2, 8), (3, 7), (4, 6), (5, 5), (6, 4), (7, 3), (8, 2), (9, 1)

code = 제너레이터를 메소드로 사용해 객체 순환하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PhoneBook:
    def __init__(self, names):
        self.names = names
        
    def iter_name_startswith(self, a):
        for name in self.names:
            if name.startswith(a):
                yield name
                
pbook = PhoneBook(['apple', 'banana', 'airplane', 'monkey'])
print(*pbook.iter_name_startswith('a'))

result =

1
apple airplane