📌 이 포스팅에서는 Python의 제너레이터(Generator)를 정리해보았습니다.
🔥 제너레이터(Generator) 란?
🔥 제너레이터(Generator) 생성하기
🔥 리스트 컴프리헨션과 제너레이터의 차이점
✔️ 제너레이터는 발전기라는 의미처럼 이 객체를 호출할 때마다 yeild가 작동되 값을 순차적으로 산출합니다.
✔️ 함수 내부에서 yield가 사용하면 그 함수는 제너레이터가 되며, 제너레이터는 이터레이터를 생성해주는 함수입니다.
✔️ 이터레이터는 클래스에 iter, next 등의 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 손쉽게 생성할 수 있다는 장점이 있습니다.
✔️ yield로 생성된 제너레이터는 이미 iter와 next를 갖고 있는 것을 아래와 같이 확인할 수 있습니다.
def generator_func(): yield 1 yield 2 yield 3 print(generator_func() # <generator object generator_func at 0x7fc786582580> print(hasattr(generator_func(), '__iter__')) # True print(hasattr(generator_func(), '__next__')) # True
✔️ yield로 생성한 함수는 제너레이터를 반환하기 때문에 iter를 사용할 필요없이 바로 next로 yield 오른쪽에 위치한 값을 순차적으로 함수 밖으로 산출해줍니다.
✔️ 함수의 return과 제너레이터의 yield의 차이점은 return은 함수가 호출되면 값을 반환하고 함수를 종료시키지만, yield는 함수 내부에서 함수 외부로 값을 순차적으로 전달해 준다는 점입니다. 뿐만아니라 send함수를 통해 mianroutine에서 값을 받아와 양방향 통신도 할 수 있습니다.
✔️ 즉, 제너레이터는 제너레이터의 객체(함수)가 호출되었을 때, yield 오른쪽의 값을 반환하고 바로 다음 yield의 위치를 기억한 상태로 다음 제네레이터 호출(실행 양보)을 기다립니다.
def generator_func(): yield 1 yield 2 yield 3 g = generator_func() # 👈 yield를 통해 생성된 제너레이터 print(g) # <generator object generator_func at 0x7fc786582580> print(g.__next__()) # 1 print(g.__next__()) # 2 print(g.__next__()) # 3
✔️ 제너레이터로 이터레이터를 만들지않으면, 선언과 동시에 메모리를 소모시킵니다. 데이터양이 많아졌을 때 아래와 같은 코드는 메모리 효율성 좋지 않습니다.
numbers = [i for i in range(10)] print(numbers) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
✔️ 이에 제너레이터를 통해 이터레이터를 생성하면 다음 순서를 기억한 상태로 객체가 생성되고, 호출하기 전에는 모든 값을 메모리에 올리지 않습니다. 즉, yield를 호출해 generator를 가동시킴으로써 값을 산출하고 그만큼의 메모리르 사용합니다.
✔️ 이를 지연 평가(lazy evaluation) 방식이라 합니다. 제너레이터 통해 이터레이터를 만들면 아래와 같습니다.
def numbers(): yield 0 yield 1 yield 2 yield 3 yield 4 yield 5 yield 6 yield 7 yield 8 yield 9 gen_numbers = numbers() # 👈 제너레이터로 이터레이터 생성 print(gen_numbers) # <generator object numbers at 0x7fcb396199e0> for i in gen_numbers: print(i, end=" ") # 0 1 2 3 4 5 6 7 8 9
✔️ while문을 사용하면 아래와같이 range처럼 작동하는 제너레이터를 생성할 수 있습니다.
def gen_range(start, stop): while start < stop: yield start start += 1 print(gen_range(0, 10)) # <generator object gen_range at 0x7fab88f219e0> res = [i for i in gen_range(0, 10)] print(res) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
✔️ 물론 제너레이터로 만들었기 때문에 next 함수로 yield 작동시키는 것도 가능합니다.
✔️ 또한 yield가 실행되 함수의 끝에 도달했는데도 제너레이터를 실행시키면 StopIteration 에러가 발생합니다.
def gen_range(start, stop): while start < stop: yield start start += 1 res = gen_range(0, 5) print(res) # <generator object gen_range at 0x7fb5990219e0> print(next(res)) # 0 print(next(res)) # 1 print(next(res)) # 2 print(next(res)) # 3 print(next(res)) # 4 print(next(res)) # StopIteration
✔️ yield를 함수 내부에서 값을 선언해 사용할 수도 있지만 메인루틴인 함수 호출 영역에서 제너레이터 내부로 값을 전달시킬 수 있습니다.
✔️ 이는 yield의 핵쿨한 기능인데, yield는 왼쪽은 변수를 할당하여 메인루틴(함수외부)에서 값을 전달받을 수 있기 때문입니다.
✔️ 이를 통해 코루틴(coroutin)의 개념인 양방향 통신이 가능합니다.
✔️ send()는 아래와 같이 사용할 수 있습니다.
def generator_send(): # 👈 파라미터는 존재하지 앖습니다. main_routine_value = 0 while True: main_routine_value = yield yield main_routine_value * 2 gen = generator_send() # 제너레이터 생성 print(gen) # <generator object generator_send at 0x7fc4ab9219e0> next(gen) print(gen.send(100)) # 200 next(gen) print(gen.send(300)) # 600
✔️ 제너레이터와 리스트 컴프리헨션으로 만든 객체가 엄청난 데이터를 가지고 있다고 가정할 때, 효율성에 있어 서로 차이가 발생합니다.
✔️ 이는 제너레이터의 경우 모든 값의 순서를 기억한 상태로 작동되기 전까지 메모리에 할당하지 않지만, 리스트 컴프리헨션은 작동되는 순간 모든 값이 메모리에 올려버리기 때문입니다.
✔️ 즉, 제너레이터는 필요한 값을 그때그때 처리하는 지연 평가 방식이 가능하기 때문에 메모리르 더 효율적으로 사용할 수 있습니다.