Generator는 lazy evaluation을 위해 사용될 수 있다. 먼저 다음의 예시를 보자.
def billion_numbers():
nums = []
i = 1
while i <= 1000000000:
nums.append(i)
i += 1
return nums
위 함수를 실행하면, 1억개의 element를 가진 list가 생성될 것이고, 컴퓨터 메모리에 저장될 것이다. 값이 실제로 쓰이기 전부터 모든 값이 생성 / 저장되기 때문에 비효율적이다. Generator를 사용하면 값이 필요할 때 생성함으로써 실행을 지연시키고, 이를 통해 효율성을 제고할 수 있다.
Generator는 list와 유사한 구조를 return하기는 하지만, 다음과 같은 차이점이 있다.
🐍 List는 생성됨과 동시에 모든 element를 저장한다. Generator는 element가 필요할 때 생성한다.
🐍 List는 계속해서 반복(iterate)할 수 있지만, generator는 한 번만 가능하다.
🐍 List는 인덱스를 통해 element를 가져올 수 있지만, generator는 불가능하다.
Generator는 값을 메모리에 저장하지 않는다. 직전에 산출했던 값을 기억하고, 다음에 실행될 때 그 다음 값을 산출한다.
def hundred_numbers_with_generator():
i = 0
while i < 100:
yield i
i += 1
print('generator: ', hundred_numbers_with_generator())
print('generator:', [x * 2 for x in hundred_numbers_with_generator()])
hundred_numbers_with_generator()의 결과는 리스트가 아니라 generator object임을 확인할 수 있다.
next()는 빌트인 함수로, '다음' 결과 값을 산출한다(중의적인 것 같아서.... next() is a built-in function, which returns the next value)
위 hundred_numbers_with_generator()에 이어서 코드를 작성하였다.
g = hundred_numbers_with_generator()
print(next(g))
print(next(g))
print(next(g))
print(list(g))
Generator는 마지막으로 산출했던 값을 기억하고 있다가, next로 호출했을 때 그 다음 값을 산출한다.
아래의 클래스는 위 generator와 똑같은 결과를 갖는다.
class first_hundred_Generator:
def __init__(self):
self.number = 1
def __next__(self): #iterator
if self.number <= 100:
current = self.number
self.number += 1
return current #returns 0
else:
raise StopIteration() #when reaching the end of generator, throw an error
my_gen = first_hundred_Generator()
print(my_gen.number) #1
my_gen.__next__()
print(my_gen.number) #2
print(next(my_gen)) #2
print(next(my_gen)) #3
print(next(my_gen)) #4
numbers_list_comprehension = [x for x in [6,7,8,9,10]]
numbers_gen_comprehension = (x for x in [6,7,8,9,10])
print(next(numbers_gen_comprehension)) #6
대괄호는 list comprehension이고 소괄호는 generator Expression이다. 소괄호라고 tuple comprehension일 것이라고 생각하지 말자.
Iterator: used to get the next value(step by step).
Iterable: used to go over all the values of the iterator
Iterator는 값을 순차적으로 꺼내올 수 있는 객체이다. Generator와 Iterator 값을 순차적으로 반환한다는 데에 공통점이 있지만, 다음과 같은 차이점이 있다.
🐍 Generator는 함수를 사용해 생성하지만 iterator는 iter()과 next() 함수를 사용해서 생성한다.
🐍 Generator에서는 yield 키워드를 사용하지만, iterator에서는 사용하지 않는다.
이 외에도 여러가지 차이점이 있지만, 쉽게 생각하면 generator는 iterator를 생성하는 함수라고 볼 수 있다. Generator를 사용하면 더 빠르고 컴팩트하게 코드를 작성할 수 있고, iterator를 사용하면 memory-efficient하다는 장점이 있다.
class FirstFiveIterator:
def __init__(self):
self.numbers = [1,2,3,4,5]
self.i = 0
def __next__(self):
if self.i < len(self.numbers):
current = self.numbers[self.i]
self.i += 1
return current
else:
raise StopIteration()
my_iter = FirstFiveIterator()
print(next(my_iter)) #1
print(next(my_iter)) #2
print(next(my_iter)) #3
print(next(my_iter)) #4
print(next(my_iter)) #5
print(next(my_iter)) #error stopIteration
Iterable은 '__iter__' method를 가지고 있는 object이다.
class FirstHundredIterable:
def __iter__(self):
#object에 __iter__ method를 사용하면, iterable이 되어 iterator을 return
return FirstHundredGenerator()
print(sum(FirstHundredIterable()))
for i in FirstHundredIterable():
print(i)
(캡쳐는 못했지만 100까지 출력되었다)
위 iterator를 사용하는 대신 generator에 __iter__ method를 추가함으로써 똑같은 결과를 출력할 수 있다.
class FirstHundredGenerator:
def __init__(self):
self.number = 1
def __next__(self): #iterator
if self.number <= 100:
current = self.number
self.number += 1
return current #returns 0
else:
raise StopIteration() #reach the end of generator
def __iter__(self):
return self
print(sum(FirstHundredGenerator()))
for i in FirstHundredGenerator():
print(i)
(또 짤렸지만 100까지 출력되었다. 진짜로.)
__len__과 __getitem__ method를 사용하여서도 object를 iterable로 만들 수 있다.
class AnotherIterable:
def __init__(self):
self.cars = ['Fiesta', 'Focus']
def __len__(self):
return len(self.cars)
def __getitem__(self, i):
return self.cars[i]
for car in AnotherIterable():
print(car)
🐍 Problem
딕셔너리도 반복가능한 객체라서 앞서본 리스트와 같이 __iter__함수와 __next__함수를 사용할 수 있고 파이썬 기본함수인 iter, next 또한 사용할 수 있습니다. 다음의 간단한 키를 출력하는 딕셔너리에 대한 for 문을 while문으로 구현해 보세요.
D = {'a':1, 'b':2, 'c':3}
for key in D.keys():
print(key)
🐍 Answer
I = iter(D.keys())
while True:
try:
X = next(I)
except StopIteration:
break
print(X)
🐍 Problem
다음 코드를 분석
import time
L = [1,2,3]
def print_iter(iter):
for element in iter:
print(element)
def lazy_return(num):
print("sleep 1s")
time.sleep(1)
return num
print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)
print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)
🐍 Answer
#코드 실행 결과
comprehension_list=
sleep 1s
#1초
sleep 1s
#1초
sleep 1s
#1초
1
2
3
generator_exp=
sleep 1s
#1초
1
sleep 1s
#1초
2
sleep 1s
#1초
3
List comprehension의 경우 sleep 1s 출력과 time.sleep이 먼저 실행되고 그 후 L에 대한 루프가 돌았다. 즉, lazy-return 함수가 먼저 반복되면서 실행되었고, lazy-return이 종료된 후에 print-iter 함수가 실행되었다. 한편 generator expression의 경우 lazy-return이 실행됨과 동시에 print_iter도 함께 반복 실행되었다. List comprehension은 lazy evaluation을 하지 않기 때문에 lazy-return이 멈추지 않고 반복되지만, generator expression은 lazy evaluation을 하기 때문에 함수가 실행되기 전까지 다음 반복을 실행하지 않는다.