본문 바로가기
[Language] - Python

# 12. 제너레이터 (Generator)

by Bebsae 2021. 4. 14.

이번 포스트에서는 제너레이터에 대해 알아보고자 한다. 필자는 제너레이터에 대해 잘 몰랐을 때 단순히 특정 loop에서 yield 키워드를 사용하면 제너레이터 인스턴스가 반환된다는 사실만 알았다. 

 

def gen():
    li = [1, 2, 3, 4, 5]
    for i in li:
        yield i
        
g = gen()
print(g)

>>
<generator object gen at 0x7f89a2f5f970>

다음과 같이 제너레이터 메소드에서 반환되는 값은 제너레이터 인스턴스이다. 이 제너레이터 인스턴스를 루프에 적용하면 다음과 같이 사용할 수 있다.

for i in gen():
    print(i)
    
>>
1
2
3
4
5

이렇게만 보면 일반적인 iterator와 다를게 없어보인다. 지금부터 제너레이터에 대해 좀더 알아보자.

 

def gen2():
    li = [1, 2, 3, 4, 5]
    yield from li

g2 = gen2()
print(g2)

for i in gen2():
    print(i)

>>
<generator object gen2 at 0x7fb4de0c0c80>
1
2
3
4
5

우선 yield from 키워드를 사용하게 되면 순차적으로 각 요소를 for loop 같은 효과를 기대할 수 있다. 근데 중요한 것은 이게 아니다. 

Lazy Evalutaion 이라는 개념이다. Eager Evaluation과 상반되는 개념이다. Eager Evaluation이 무엇인지 살펴보자.

 

def return_func():
    print('return!')
    return 1

li_com = [return_func() for i in range(10)]
print(li_com)

>>
return!
return!
return!
return!
return!
return!
return!
return!
return!
return!
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

list comprehension을 사용하여 특정 함수의 리턴값을 요소로 채우는 경우, 순차적으로 return_func 메소드가 호출되고 반환되는 값이 리스트에 채워지는 형식이다. 즉, 채워넣는 즉시 해당 메소드를 급하게 (Eager) 채워넣는 것이다. 

 

gen_com = (return_func() for i in range(10))
print(gen_com)

>>
<generator object <genexpr> at 0x7fd3b557f3c0>

반면 제너레이터 표현식을 사용하면 renturn_func() 메소드는 호출되지 않는 것을 확인할 수 있다. (reutrn! 문자열이 출력되지 않으므로) 즉, 메소드를 즉시 실행하지 않고 필요할때(Lazy) 실행한다는 의미이다. return_func() 메소드 인스턴스 10개가 제너레이터에 들어갔을 뿐,호출되지 않은 상태라고 이해하면 쉽다. 그럼 언제 호출되느냐?

 

next(gen_com)

>>
return!

next() 메소드의 인자로 제너레이터 인스턴스가 대입되었을 때 호출된다! 이밖에도 for loop에 적용되었을 때도 호출된다.

 

gen_com = (return_func() for i in range(10))
print(gen_com)

for i in gen_com:
    print(i)
    
>>
<generator object <genexpr> at 0x7fabccd7f3c0>
return!
1
return!
1
return!
1
return!
1
return!
1
return!
1
return!
1
return!
1
return!
1
return!
1

for loop를 돌면서 내부적으로 next()가 호출되는 것을 유추할 수 있다. 그리고 루프를 돌때마다 메소드가 호출되어 return! 이 출력되고 반환값 1이 출력되는 것을 확인할 수 있다.

 

그렇다면 여기서 의문, 왜 제너레이터를 사용할까?

Eager Evaluation 같은 경우에는 각 요소들이 추가되기 위해 10번만큼 메소드가 호출되었다. 하지만, 10개를 전부 사용하지 않는 경우라면?

필요한 횟수만큼만 메소드를 호출하면 된다. 이때 사용하는 것이 제너레이터이다. 비록 10개의 메소드 인스턴스를 제너레이터에 포함시켰지만, 내가 필요한 횟수가 3번이라면 3번만 호출하면 되는 문제가 아닌가?

for i, v in enumerate(gen_com):
    if i == 3:
        break
    print(v)
    
 >>
return!
1
return!
1
return!
1
return!

표면적으로  코드만 보았을 때는 리스트를 순회하는 것과는 특별히 달라보일 것이 없다. 하지만, iterator와는 다르게 딱 필요한 횟수만큼만 return_func() 메소드를 호출한 것을 확인할 수 있다. 여담으로 마지막에 return! 이 한번 더 출력된 이유는 다시 루프를 돌아 if문을 만나기 전에 내부적으로 next()가 호출되었기 때문이다. Lazy Evaluation을 활용하면 메모리 관리에 효율적이다. 

 

Python이외에 다른 언어에서도 Lazy Evaluation개념은 존재한다. 이를테면 js에는 stream()이 있다.

댓글