Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

파이썬 제너레이터 완벽 이해하기: yield 키워드부터 메모리 최적화 활용까지

제너레이터(Generator)는 PEP 255와 함께 도입된 이후 파이썬에서 중요한 역할을 해왔습니다.

파이썬의 제너레이터는 루프의 반복 동작을 제어하는 데 사용할 수 있는 특수한 루틴입니다. 제너레이터는 배열을 반환하는 함수와 비슷하지만, 결정적인 차이가 있습니다. 전체 배열을 한 번에 반환하는 일반 함수와 달리, 제너레이터는 한 번에 하나의 값만 산출(yield)하기 때문에 메모리 사용량이 훨씬 적습니다.

"yield" 키워드를 포함하는 모든 파이썬 함수를 제너레이터라고 부를 수 있습니다. 일반적인 파이썬 함수는 첫 줄부터 실행을 시작해 return 문이나 예외 발생, 또는 함수의 끝에 도달할 때까지 실행되며, 함수 스코프 내에서 생성된 지역 변수들은 모두 소멸되어 더 이상 접근할 수 없습니다. 반면 제너레이터는 yield 키워드를 만나는 순간 함수의 상태가 고정(freeze)되고, 제너레이터가 다시 호출될 때까지 모든 변수가 메모리에 보관됩니다.

제너레이터는 이터레이터와 함께 사용하거나 "next" 키워드로 명시적으로 호출할 수도 있습니다.

일반적으로 파이썬의 제너레이터는 다음과 같은 특징을 가집니다.

  • def 키워드로 정의됩니다
  • yield 키워드를 사용합니다
  • 여러 개의 yield 키워드를 포함할 수 있습니다
  • 이터레이터를 반환합니다

이터레이터와 함께 사용하는 제너레이터

def generator_thr_iter():
    yield 'xyz'
    yield 246
    yield 40.50
for i in generator_thr_iter():
    print(i)

출력 결과

xyz
246
40.5

next()를 사용한 제너레이터

def generator_thr_iter():
    yield 'xyz'
    yield 246
    yield 40.50
>>> g = generator_thr_iter()
>>> g.__next__()
'xyz'
>>> g.__next__()
246
>>> g.__next__()
40.5
>>> g.__next__()
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
g.__next__()
StopIteration

제너레이터는 모든 항목을 한꺼번에 반환하는 대신 하나씩 순차적으로 값을 넘겨준다고 생각할 수 있습니다. 다음 항목이 요청될 때까지 제너레이터 함수는 일시 중지된 상태를 유지합니다. 모든 값을 소진한 후 다시 호출하면 위 예제처럼 StopIteration 예외가 발생합니다.

실전 예제: 1부터 n까지 숫자의 제곱 출력하기

1부터 n까지의 숫자 제곱을 계산하는 상황을 가정해 봅시다. 만약 n이 매우 큰 숫자라면, n까지의 숫자 리스트를 생성하는 것만으로 시스템 메모리 전체를 차지할 수 있습니다.

제너레이터 없이 접근한다면 다음과 같이 작성할 것입니다.

>>> n = 200000000000
>>> number_list = range(1, n+1)
>>> for i in number_list:
    print(i*i)

위 방식은 상당한 시스템 메모리를 소비합니다. 더 나은 방법은 숫자 리스트를 아예 생성하지 않으면서 숫자를 순회하는 것입니다. 바로 이럴 때 제너레이터가 빛을 발합니다.

동일한 작업을 수행하는 제너레이터 기반 코드는 다음과 같습니다.

def num_generator(n):
    num = 1
    while True:
        yield num
        if num == n:
            return
        else:
            num += 1
for i in num_generator(200000000000):
    print(i*i)

실행 흐름 단계별 분석

for 루프가 처음 초기화되면 num_generator가 호출되고, n = 200000000000 값이 메모리에 저장됩니다. 이후 num = 1로 초기화된 채 무한히 반복되는 while 루프에 진입합니다. yield num을 만나는 순간 while 루프는 고정되고 모든 지역 변수가 메모리에 저장됩니다. 현재 num = 1이므로 yield num이 for 루프로 반환되어 i에 할당되고, 1의 제곱(1)이 출력된 후 num_generator가 다시 호출됩니다.

다음 호출부터는 이전에 고정되었던 지점부터 실행이 재개됩니다. 따라서 num == n (1 == 200000000000) 조건을 검사하는데, 거짓이므로 num += 1이 실행되어 num = 2가 되고 while 루프가 다시 실행됩니다. 이 과정이 계속 반복됩니다.

마지막으로 while 루프는 n = 200000000000까지 실행되고, 200000000000이 산출된 후 'num == n' (200000000000 == 200000000000) 조건이 참이 되어 return 문이 실행됩니다.

제너레이터가 return 문을 실행하거나 예외를 만나거나 제너레이터의 끝에 도달하면 "StopIteration" 예외가 발생하고, for 루프의 반복은 그 순간 중단됩니다. 결과적으로 위 코드는 대규모 숫자 리스트를 생성하지 않고도(생성했다면 막대한 시스템 메모리를 차지했을 것입니다) 200000000000까지의 숫자 제곱을 모두 출력할 수 있었습니다.

이처럼 제너레이터를 일상적인 프로그래밍에 활용하면 메모리 효율이 뛰어난 프로그램을 작성할 수 있습니다. 특히 대용량 데이터 처리나 무한 시퀀스 생성이 필요한 경우 제너레이터는 강력한 해결책이 됩니다.