Generators

Junyoung Kim·2022년 1월 5일
0

Python+

목록 보기
3/7

파이썬에서 보통의 함수는 값을 반환하고 종료하지만, 제너레이터 함수는 값을 반환하기는 하지만 산출(yield)한다는 차이점이 있다. 쉽게 말하면 제너레이터는 iterator를 생성해주는 함수라고 볼 수 있다. 다음 코드를 확인해보자.

def generator_squares():
    for i in range(3):
        yield i ** 2

print("gen object=", end=""), print(generator_squares())

gen object=<generator object generator_squares at 0x10f3b0150>

yield는 제너레이터 함수에서 값을 반환할 때 사용되며 yield 호출 후에 다시 next가 호출될 때 까지 현재의 상태에서 머물고 있다가 next 함수가 호출되면 이전의 상태에 이어서 다음 연산을 수행한다.

dir로 함수 종류를 확인해보면 iternext함수 둘 다 존재하는 것을 확인할 수 있다.

print("dir gen =", end=""), print(dir(generator_squares()))

dir gen =['__class__', '__del__', '__delattr__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__',
'__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

그래서 이터레이터에서 처럼 __iter__를 호출한 후에 __next__함수를 호출하지 않아도 __next__를 바로 호출할 수 도 있다. 다음은 iternext 함수를 사용한 제너레이터 함수의 코드다.

gen = generator_squares()
print(gen.__next__())
print(gen.__next__())
print(gen.__next__())
print(gen.__next__())

출력결과
0
1
4
Traceback (most recent call last):
  File "generator.py", line 14, in <module>
    print(gen.__next__())
StopIteration

제너레이터 함수는 실행중에 send 함수를 통해서 값을 전달할 수도 있다.


def generator_send():
    received_value = 0

    while True:

        received_value = yield
        print("received_value = ",end=""), print(received_value)
        yield received_value * 2
        
gen = generator_send()
next(gen)
print(gen.send(2))

next(gen)
print(gen.send(3))

출력결과
received_value = 2
4
received_value = 3
6

generator_send 함수는 yieldsend를 통해서 받은 값을 received_value에 할당하고 그 값의 2배 수를 리턴받고 있다.

제너레이터에서는 이처럼 yield를 이용해서 제너레이터 함수 실행 중에 값을 전달 할 수 있고, 응용하면 제너레이터 함수를 사용해서 main 실행 루프에서 연산결과에 따라 호출도 제어할 수 있다.

이외에도 제너레이터 표현식(generator experession)이 있는데, 제너레이터 표현식은 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)

출력결과
sleep 1s
sleep 1s
sleep 1s
1
2
3
generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3

python 내장함수 time을 import한 뒤 lazy_return함수에 1초 지연을 적용시켰다. comprehension_list는 대괄호로 리스트 컴프레헨션을 적용해서 1초 간격으로 sleep 1s 문장이 출력된 후 1,2,3의 결과가 한꺼번에 출력이 되었다.
generator_exp는 소괄호로 제너레이터 표현식을 적용해서 sleep 1s와 숫자가 같이 출력된 뒤에, 1초 간격으로 후속 문장이 출력되었다.

Lazy evaluation은 직역하면 '느긋한 계산'인데, 달리 말하면 '필요한 것'만 계산한다는 의미다. 컴파일러가 무한하게 반복하는 객체나 크기가 큰 객체에서 필요없는 계산을 하지 않음으로써 메모리 값을 아낄 수 있어서 성능의 이득을 취할 수 있다.
Lazy evaluation을 사용하면 무한 자료 구조의 일부를 사용할 수 있는데 다음 예시를 살펴보자.

def addOne(n):
    [n] + addOne(n + 1)

list = addOne(1) // [1, 2, 3, 4, 5, 6, …]

oneToThree = list.takeFirst (3) 
print (oneToThree)  
print (range (5, 10)) 

출력결과
[1, 2, 3]
[5, 6, 7, 8, 9]

무한하게 반복하는 자료구조인 addOne 함수에서 print (range (5, 10))와 같이 범위를 지정하여 [5, 6, 7, 8, 9]의 결과값을 산출하였다. 위처럼 무한한 데이터 중에서 특정 데이터를 선택하거나 혹은 데이터의 범위를 정하여 그 일부를 사용할 수 있다.

이제 다시 generator_exp으로 돌아가보자.
리스트 컴프리헨션을 사용한 comprehension_listsleep을 통해 지연이 적용된 첫번째부터 3번째 문자열을 제외하고 1,2,3은 한꺼번에 출력이 되었다. 이는 컴파일러가 sleep 이전에 해당 print 명령을 수행해 놓았고, sleep이 끝나자 한번에 출력한 것이다.

그러나 generator_exp는 '필요한 계산'만 수행해서, print_iter 함수를 미리 수행하지 않고, lazy_return 함수가 수행될 때마다 하나씩 호출하여 수행한 것이다. 따라서 lazy_return 함수의 print문인 sleep 1s와 숫자가 같이 출력된 뒤에, 1초 간격으로 print_iterprint문이 출력되는 결과가 나온 것이다.

0개의 댓글