제너레이터는 반복자(iterator)와 같은 루프의 작용을 컨트롤하기 위해 쓰여지는 특별한 함수 또는 루틴이다. 사실 모든 제너레이터는 iterator이다.
제너레이터는 배열이나 리스트를 리턴하는 함수와 비슷하며, 호출을 할 수 있는 parameter를 가지고 있고, 연속적인 값들을 만들어 낸다. 하지만 한번에 모든 값을 포함한 배열을 만들어서 리턴하는 대신에 yield 구문을 이용해 한 번 호출될 때마다 하나의 값만을 리턴하고, 이런 이유로 일반 반복자에 비해 아주 작은 메모리를 필요로 한다. (Wikipedia)
일반함수 : 실행된 후 모든 컨트롤을 호출자(caller)에게 리턴하며, 동시에 함수에 포함된 내부 함수나 로컬 변수가 메모리상에서 사라지게 된다.
제너레이터 : 실행되도 함수나 로컬변수가 메모리에서 사라지지 않고, 대기하고 있다가 next
를 통하여 호출되면 이어서 실행한다. 따라서 매번 다시 시작하는 일반 함수와 비교하여, 메모리 리소스를 크게 절약할 수 있다.
먼저 일반함수의 형태를 살펴보자. square_numbers(nums)
함수는 nums에 받은 배열의 요소를 for문을 돌면서 제곱하여 그 값을 리턴한다. 함수를 실행하는 순간에, 모든 값이 배열에 담겨 리턴된다.
# 일반함수
def square_numbers(nums):
result = []
for i in nums:
result.append(i * i)
return result
my_nums = square_numbers([1, 2, 3, 4, 5])
print my_nums
> [1, 4, 9, 16, 25]
위의 함수를 제너레이터로 작성하면 다음과 같다. 새로운 결과값을 담는 results
배열은 불필요하며, return
대신에 yield
를 사용하여 각각의 요소를 제곱한다. print
를 찍어 보았을때, 결과값이 아닌 generator object
를 리턴하며, 각각의 요소의 제곱값을 불러오기 위해서 next
를 사용한다.
# 제너레이터
def square_numbers(nums):
for i in nums:
yield i * i
my_nums = square_numbers([1, 2, 3, 4, 5])
print my_nums
> <generator object square_numbers at 0x1007c8f50>
print next(my_nums)
> 1
print next(my_nums)
> 4
더이상 리턴할 요소가 없는데 next
를 호출한 경우, StopIteration
에러가 뜬다.
...
print next(my_nums)
> 1
print next(my_nums)
> 4
print next(my_nums)
> 9
print next(my_nums)
> 16
print next(my_nums)
> 25
print next(my_nums)
Traceback (most recent call last):
File "generator.py", line 12, in
print next(my_nums)
StopIteration
제너레이터는 일반적으로 for문을 통해서 호출하여 사용한다. 이렇게 호출하면 StopIteration을 만날일이 없다!
def square_numbers(nums):
for i in nums:
yield i * i
my_nums = square_numbers([1, 2, 3, 4, 5])
for num in my_nums:
print num
같은 코드를 list comprehension 으로 작성하면 더욱 pythonic해진다.
[]이 아니라 ()를 사용한것을 주목하자. []를 사용할경우 my_nums는 리스트에 담겨서 출력된다.
리스트로 변형할 경우 보기는 편하지만, 정작 제너레이터를 쓰는 의미가 사라진다. 왜냐하면, 결국 함수를 호출할 때 결과값을 한꺼번에 메모리에 저장하는 꼴이 되기 때문이다.
my_nums = (x*x for x in [1, 2, 3, 4, 5])
print my_nums
> <generator object <genexpr> at 0x1007c8f50>
for num in my_nums:
print num
>1
4
9
16
25
참고자료: