A function which returns an iterator. It looks like a normal function except that it contains yield statements for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function. Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator resumes, it picks-up where it left-off (in conrast to functions which start fresh on every invocation).
generator는 iterator를 반환하는 함수이다. iterator란 next()
메소드로 데이터를 순차적으로 호출할 수 있는 object이다.
generator는 일반 함수와 비슷하게 생겼지만 return이 아닌 yield를 사용한다. 실행중 yield를 만나면 함수는 정지되고 해당 값을 next()
를 호출한 곳으로 보내준다. 함수는 종료된 것이 아니므로 함수에서 사용된 데이터들은 지워지지 않고 남아있는 상태이다.
def generator_squres():
for i in range(1, 4):
yield i ** 2
print(generator_squres())
<generator object generator_squres at 0x7ff306726c80>
위와 같이 일반적인 함수에서처럼 호출만하면 값이 반환되지 않는다.
gen = generator_squres()
print(next(gen)) # 1st
> 1
next()
를 호출한 곳으로 값을 반환해준다. yield를 실행하고 멈춘 상태이므로 next()
를 한번 더 호출하면 다음 연산이 진행된다.
print(next(gen)) # 2nd
print(next(gen)) # 3rd
print(next(gen)) # 4th
> 4
9
Traceback (most recent call last):
File "main.py", line 25, in <module>
print(next(gen))
StopIteration
👉 StopIteration: 더이상 가져올 값이 없는 것을 의미한다.
list comprehension처럼 generator도 generator experssion으로 더 쉽게 사용할 수 있다. list comprehension과 비슷하게 생겼지만 [
,]
대신 (
,)
이 온다.
generator_expression = ( i for i in range(5) )
list_comprehension = [x for x in range(5)]
print(list_comprehension)
> [0, 1, 2, 3, 4]
generator_expression = ( x for x in range(5) )
print(generator_expression)
> <generator object <genexpr> at 0x7efe31b9ac80>
list comprehension에서는 만들 수 있는 모든 값이 계산되어 리스트안에 들어가지만 generator expression은 generator object를 return하기 때문에 next()
를 사용할때만 함수를 실행시켜 값을 생성한다.
generator는 모든 값을 메모리에 저장하지 않기 때문에 메모리를 효율적으로 사용할 수 있다. 리스트는 사이즈에따라 사용하는 메모리가 늘어나지만 generator는 next()
메소드로 값에 접근할 때마다 값을 메모리에 적재하기 때문에 사이즈가 늘어나더라도 메모리는 동일하게 사용된다. 따라서 list 의 규모가 큰 값을 다룰 수록 generator의 효율성은 더욱 높아진다.
+) performance에 대한 이점은 없을까?
➡ Generator는 프로그램의 성능을 올리기위해 사용되지 않는다.
또한 결과 값이 실제로 쓰일 때까지 계산을 늦추는 Lazy evaluation이 가능해진다.
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) # print_iter 호출시 generator exp 실행됨
comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3
list comprehension의 경우에는 list의 모든 값을 한번에 수행하기 때문에 L
의 사이즈가 클수록 소요되는 시간이 길어진다. list를 얻기 위해서는 [len(L)
*lazy_return(i)에 걸리는 시간
]만큼을 기다려야 한다. 그러므로 시간이 많이 걸리는 함수를 실행해야하거나 list의 사이즈가 매우 클 때 부담이 될 수 있다.
하지만 generator에서는 하나씩 lazy_return(i)
이 수행되기때문에 수행 시간이 긴 연산이 있다면 지연시켜 필요할때만 실행하면 되고 불필요한 값들이 계산되는 시간을 기다릴 필요가 없다.
generator의 정의를 봤을때 iterator를 리턴하는 함수였고 iterator는 next()
를 호출해야 다음 값으로 넘어갈 수 있었다. 근데 코드를 살펴보니 print_iter()
에는 next()
가 없는데 어떻게 한개씩 잘 넘어갈 수 있는지 이해가 되지 않았다.
찾아보니 for문이 iterator의 next()
메서드를 호출하는 역할까지 하고 있다는 것을 알게되었다.
실제 for 루프에 Iterable Object를 사용하면, 해당 Iterable의
__iter__()
메서드를 호출하여 iterator를 가져온 후 그 iterator의next()
메서드를 호출하여 루프를 돌게 된다.(next()
와iter()
메소드를 생성하면 iterables를 정의할 수 있다.)
print(dir(generator_exp))
> [... '__iter__', ... '__next__'...]
그러므로 generator_exp로 generator를 생성하면 주소만 할당되고 어떠한 값도 생성되지 않은 상태이다. print_iter(generator_exp)
를 실행하면 해당 iterator(generator_exp)를 for문이 돌면서 next()
메소드를 호출하여 lazy_return
이 실행되고 그 다음에 print(element)
가 발생하게 되는 것이었다.
여기에 for loop이 어떻게 작동하는지 더 정리해보았다.
[참조]