개발

Python - 제너레이터

kwony 2023. 2. 14. 16:49

제너레이터

 

파이썬에는 return 대신 yield 로 결과 값을 리턴하는 코드가 있는데 이 함수는 제너레이터를 생성하는 함수다. 제너레이터는 일반적으로 알고 있는 리스트랑 유사하다. 

 

def multiple_list(number, limit):
    counter = 1
    value = number * counter

    ret = []

    while value <= limit:
        ret.append(value)
        counter += 1
        value = number * counter

    return ret

def multiple_generator(number, limit):
    counter = 1
    value = number * counter

    while value <= limit:
        yield value
        counter += 1
        value = number * counter

 

위의 코드에서 multiple_list와 multiple_generator는 모두 내부 로직에서 차이가 없다. 대신 multiple_list는 결과를 리스트로 줬고 아래 multiple_generator는 결과 값을 제너레이터로 준다. 

 

ml = multiple_list(500, 5000)
mg = multiple_generator(500, 5000)

for ml_num, mg_num in zip(ml, mg):
    print(f"ml_num: {ml_num}, mg_num: {mg_num}")


ml_num: 500, mg_num: 500
ml_num: 1000, mg_num: 1000
ml_num: 1500, mg_num: 1500
ml_num: 2000, mg_num: 2000
ml_num: 2500, mg_num: 2500
ml_num: 3000, mg_num: 3000
ml_num: 3500, mg_num: 3500
ml_num: 4000, mg_num: 4000
ml_num: 4500, mg_num: 4500
ml_num: 5000, mg_num: 5000

 

실행한 결과 출력값도 동일하다. 제너레이터나, 리스트나 결과 값이 동일하다면 굳이 따로 가져갈 이유는 없다. 그런데 제너레이터는 리스트와 결과물을 가져오는 방식이 다르다. 제너레이터는 데이터를 호출하는 시점에 코드를 실행하고 그 전까지는 함수 실행을 중지한다. for 문을 돌기 전까지는 어떤 코드를 돌릴 예정이라고만 알릴 뿐 실제로 어떤 값을 갖고 있는지는 모른다. 

 

ml = multiple_list(500, 5000)
mg = multiple_generator(500, 5000)

print(f"ml: {ml}")
print(f"mg: {mg}")

#######################################

ml: [500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000]
mg: <generator object multiple_generator at 0x105142a40>

 

print로 값을 출력해보면 리스트는 이미 결과 값을 갖고 있는 반면 제너레이터는 결과 값은 없고 주소값만 딸랑 있다. 아직 어떤 데이터가 있는지 모르기 때문이다. 

 

import time

def print_time(*args):
    print(time.strftime("%c", time.localtime(time.time())), *args)


def multiple_generator(number, limit):
    counter = 1
    value = number * counter

    while value <= limit:
        yield value
        print_time(f"yield value {value}")
        counter += 1
        value = number * counter

mg = multiple_generator(500, 5000)

print(mg)

for num in mg:
    print_time(f"print_num {num}")
    sleep(1)

 

자세한 제너레이터 동작 방식을 알기 위해 중간에 sleep을 넣어봤다. 수정한 multiple_generator 함수에는 yield 바로 아래 print_time를 넣었다. 이 함수는 값을 호출하는 것 뿐만 아니라 호출하는 시점의 시간도 같이 출력한다. multiple_generator 상에서 값을 yield value를 호출하는 부분과 print_time 사이에 어떤 코드도 없기 때문에 로그상으로는 두 코드 호출 시점에 시간차가 없어야 한다. 그런데 실제 결과 값은 다음과 같다. 

 

Tue Feb 14 16:24:17 2023 print_num 500
Tue Feb 14 16:24:18 2023 yield value 500
Tue Feb 14 16:24:18 2023 print_num 1000
Tue Feb 14 16:24:19 2023 yield value 1000
Tue Feb 14 16:24:19 2023 print_num 1500
Tue Feb 14 16:24:20 2023 yield value 1500

 

print_num 이 호출되는 시점과 yield value가 호출되는 시점 사이에 1초간 차이가 난다. 1초는 for 문 내에서 딜레이를 1초간 줬기 때문에 발생한 것이다. 실제 코드는 아래 순서대로 동작한다. 

 

1. yield value

2. for 문 내의 sleep(1)

3. counter += 1

4. value = number * counter

5. yield value

 

제너레이터에선 호출 시점에 데이터를 접근하고 그전까지는 함수의 실행을 중지한다. yield를 기점으로 왔다갔다 하는 방식이다. 리스트와 다르게 모든 데이터를 한번에 리턴하지 않아도 되기 때문에 메모리를 효율적으로 사용할 수 있다. 

 

인공지능의 경우 대량의 데이터를 가져와야하는 경우가 있는데 yield를 적절하게 사용하면 메모리 공간을 아끼면서 처리할 수 있어서 유용할 것 같다. 왜 인공지능 라이브러리들은 대부분 파이썬인지 알 것 같다.