TIL30. Python : 제너레이터(Generator) 개념 정리

ID짱재·2021년 10월 10일
1

Python

목록 보기
36/39
post-thumbnail

📌 이 포스팅에서는 Python의 제너레이터(Generator)를 정리해보았습니다.



🌈 제너레이터(Generator) 개념 정리

🔥 제너레이터(Generator) 란?

🔥 제너레이터(Generator) 생성하기

🔥 리스트 컴프리헨션과 제너레이터의 차이점



1. 제너레이터(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는 오른쪽값을 함수 밖으로 산출(생산해서 반환하고)하고, 실행을 양보합니다.

✔️ 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


2. 제너레이터(Generator) 생성하기

🤔 왜 제너레이터를 사용해서 이터레이터를 만들어야할까?

✔️ 제너레이터로 이터레이터를 만들지않으면, 선언과 동시에 메모리를 소모시킵니다. 데이터양이 많아졌을 때 아래와 같은 코드는 메모리 효율성 좋지 않습니다.

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 

🤔 제너레이터로 range 함수 만들기

✔️ 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

🤔 send() 함수로 메인루틴에서 메인루틴으로 값 전달하기

✔️ 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


3. 리스트 컴프리헨션과 제너레이터의 차이점

🤔 메모리 효율성 : 제너레이터 > 리스트 컴프리헨션

✔️ 제너레이터와 리스트 컴프리헨션으로 만든 객체가 엄청난 데이터를 가지고 있다고 가정할 때, 효율성에 있어 서로 차이가 발생합니다.
✔️ 이는 제너레이터의 경우 모든 값의 순서를 기억한 상태로 작동되기 전까지 메모리에 할당하지 않지만, 리스트 컴프리헨션은 작동되는 순간 모든 값이 메모리에 올려버리기 때문입니다.
✔️ 즉, 제너레이터는 필요한 값을 그때그때 처리하는 지연 평가 방식이 가능하기 때문에 메모리르 더 효율적으로 사용할 수 있습니다.

profile
Keep Going, Keep Coding!

0개의 댓글