본문 바로가기
Python/기본문법

Generator (제너레이터)

by yororing 2024. 5. 3.

01 Generator란

1. 정의

  • '발전기' → 이 객체를 호출할 때마다 yield가 작동되어 값을 순차적으로 산출함
  • yield 키워드 사용하며 iterator를 생성해주는 함수
  • iterator는 class에서 iter, next 등의 메서드를 구현해야 하지만 generator는 함수 안에서 yield라는 키워드만 사용하면 iter, next 등의 메서드를 쉽게 생성 가능 (yield로 생성된 generator는 이미 iter, next를 갖고 있음)
def generator_func():
    yield 1
    yield 2
    yield 3
print(generator_func()) # <generator object generator_func at 0x7f37881a0fa0>
print(hasattr(generator_func(), '__iter__')) # True
print(hasattr(generator_func(), '__next__')) # True
  • yield로 생성된 generator는 이미 iter와 next를 갖고 있는 것을 확인

2. 문법

  • yield는 오른쪽 값을 함수 밖으로 산출 (i.e., 생성 후 반환)하고 실행을 양보함
  • yield로 생성한 함수는 generator를 반환하기 때문에 iter를 사용할 필요 없이 바로 next로 yield 오른쪽에 위치한 값을 순차적으로 함수 밖으로 산출함
  • 함수의 return과 generator의 yield의 차이점:
    • return은 함수가 호출되면 값을 반환하고 함수를 종료시킴
    • yield는 함수 내부에서 함수 외부로 값을 순차적으로 전달해줌; send 함수를 통해 mianroutine에서 값을 받아와 양방 통신도 가능
  • 즉, generator는 generator의 객체(함수)가 호출되었을 때 yield 오른쪽의 값을 반환하고 바로 다음 yield의 위치를 기억한 상태로 다음 generator 호출(실행 양보)을 기다림
def generator_func():
    yield 1
    yield 2
    yield 3
g = generator_func()
print(g) 		# <generator object generator_func at 0x7f6948b5a200>
print(g.__next__())	# 1
print(g.__next__())	# 2
print(g.__next__())	# 3
print(g.__next__())	

# 마지막 print(g.__next__()) 출력값
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

3. 사용 예시

def numbers():
    for i in range(4):
        yield i

# 위는 다음과 같음
def numbers():
    yield 0
    yield 1
    yield 2
    yield 3

gen_numbers = numbers()
print(gen_numbers)	    # <generator object numbers at 0x7f6948b5a2b0>

for i in gen_numbers:
    print(i, end=" ")	  # 0 1 2 3
# 각 값 리스트에 담기
gen_numbers_in_list = list(numbers())
gen_numbers_in_list      # [0, 1, 2, 3, 4]

# 다음은 안 됨 주의
gen_numbers_in_list = [numbers()]
gen_numbers_in_list      # [<generator object numbers at 0x7f4c1efd2990>]

4. 특징

  • iterable한 순서가 지정됨 (모든 Generator = iterator)
  • 느슨하게 평가됨 (순서의 다음 값은 필요에 따라 계산됨)
  • 함수의 내부 로컬 변수를 통해 내부 상태가 유지됨
  • 무한한 순서가 있는 객체 모델링 가능 (명확한 끝이 없는 데이터 스트림)
  • 자연스러운 스트림 처리를 위 파이프라인으로 구성 가능 (java에서 파일 스트림 처리 시 특정 바이트 단위로 반복하는 것)

5. 사용 이유 - 메모리 효율성

  • 메모리 효율성!
    • generator로 iterator를 만들지 않을 경우 선언과 동시에 메모리를 소모시킴
    • 데이터 양이 많을 경우 다음과 같은 코드는 메모리 효율성에 좋지 않음
numbers = [i for i in range(10)]
print(numbers) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  •  지연 평가 방식 (lazy evaluation)을 통한 메모리 효율성!
    • 그러나 generator를 통해 iterator 생성 시 다음 순서를 기억한 상태로 객체가 생성되며 호출하기 전에는 모든 값을 메모리에 올리지 않음
    • 즉, yield를 호출헤 generator를 가동시킴으로서 값을 산출하고 그만큼의 메모리만 사용
    • 이러한 방식을 지연 평가 방식 (lazy evaluation) 이라 함
def numbers():
    yield 0
    yield 1
    yield 2
    yield 3
    
# 더 간결하게
def numbers():
    for i in range(4):
        yield i

gen_numbers = numbers()
print(gen_numbers)	    # <generator object numbers at 0x7f6948b5a2b0>

for i in gen_numbers:
    print(i, end=" ")	# 0 1 2 3
  • 결론: 메모리 효율성 (generator > list comprehension)
    • generator와 list comprehension으로 만든 객체가 엄청난 데이터를 가지고 있다고 가정 시 효율성에 있어서 차이 발생
    • 이는 generator의 경우 모든 값의 순서를 기억한 상태로 작동되기 전까지 메모리에 할당하지 않으나 list comprehension은 작동되는 순간 모든 값이 메모리에 올라가기 때문
    • 즉 generator는 필요한 값을 그때 그때 처리하는 지연 평가 방식이 가능하기 때문에 메모리를 더 효율적으로 사용 가능

참조

  1. https://wikidocs.net/16069 
  2. https://velog.io/@jewon119/TIL30.-Python-제너레이터Generator-개념-정리
  3.