Python Generator

rang-dev·2020년 6월 1일
0

Generator

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: 더이상 가져올 값이 없는 것을 의미한다.

Generator Expression

list comprehension처럼 generator도 generator experssion으로 더 쉽게 사용할 수 있다. list comprehension과 비슷하게 생겼지만 [,]대신 (,)이 온다.

generator_expression = ( i for i in range(5) )

list comprehension과의 차이점

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 expression의 장점

generator는 모든 값을 메모리에 저장하지 않기 때문에 메모리를 효율적으로 사용할 수 있다. 리스트는 사이즈에따라 사용하는 메모리가 늘어나지만 generator는 next() 메소드로 값에 접근할 때마다 값을 메모리에 적재하기 때문에 사이즈가 늘어나더라도 메모리는 동일하게 사용된다. 따라서 list 의 규모가 큰 값을 다룰 수록 generator의 효율성은 더욱 높아진다.

+) performance에 대한 이점은 없을까?

➡ Generator는 프로그램의 성능을 올리기위해 사용되지 않는다.

또한 결과 값이 실제로 쓰일 때까지 계산을 늦추는 Lazy evaluation이 가능해진다.

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와 for문?

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이 어떻게 작동하는지 더 정리해보았다.


[참조]

profile
지금 있는 곳에서, 내가 가진 것으로, 할 수 있는 일을 하기 🐢

0개의 댓글