반응형

 

https://comgu.tistory.com/entry/Python-Iteration의-원리Iterator-Protocol

 

[Python] Iteration의 원리(Iterator Protocol)

Python에서 iteration(반복)은 iterator 프로토콜을 기반으로 동작한다.이 원리를 이해하려면 아래 개념들을 알아야 한다. 1. Iterable (Iterate 가능한 객체)Iterable은 __iter__() 메서드를 구현한 객체이다. for

comgu.tistory.com

 

앞서 파이썬에서 iteration이 어떻게 동작하는지, iterator protocol을 중심으로 살펴보았다.

본 글에서는 파이썬의 이터러블(iterable)이면서 동시에 이터레이터(iterator)인 제너레이터에 대해 알아본다.

 

제너레이터(Generator)

제너레이터는 이터레이터를 생성하는 함수로, 데이터를 한 번에 하나씩 생성하여 메모리를 효율적으로 사용할 수 있다.

제너레이터는 일반 함수와 비슷하지만, 데이터를 반환할 때 return 대신 yield 키워드를 사용한다.

아래의 기본적인 사용 방법을 보자.

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()  # 제너레이터 객체 생성

print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3
print(next(gen))  # StopIteration 익셉션 발생
1
2
3
StopIteration 익셉션 발생

동작 과정

  1. yield는 값을 반환하면서 함수의 실행을 멈춘다.
  2. 다음 호출 시 함수는 이전에 멈췄던 위치부터 실행을 재개한다.
  3. 더 이상 yield 값이 없으면 StopIteration 예외가 발생한다.

 

제너레이터의 특징

  1.  효율적인 메모리 사용: 한 번에 모든 데이터를 메모리에 올리지 않고 필요할 때마다 하나씩 생성한다.
  2. 이터러블(Iterable)하다: for 루프와 같은 이터레이션 구조에서 사용할 수 있다.
  3. 상태 유지: 호출될 때마다 마지막으로 멈췄던 위치를 기억한다.

 

제너레이터의 활용 예시

1. 대규모 데이터 처리

제너레이터를 사용하면 대규모 데이터를 메모리 사용 낭비 없이 처리할 수 있다.

def read_large_file(file_name):
    with open(file_name) as file:
        for line in file:
            yield line.strip()

for line in read_large_file("large_file.txt"):
    print(line)

read_large_file() 함수는 제너레이터 함수이다. yield를 사용해서 한 번에 하나의 라인을 반환한다.

yield를 통해 한 줄을 처리하고 나서 함수의 상태를 보존한 채 대기하고, 다음 호출이 오면 그 상태에서 재개한다.

  • 참고로 file에 대해서 for loop이 동작할 수 있는 이유는, file 객체 자체가 iterable이기 때문이다. 파이썬의 파일 객체는 내부적으로 iterator로 동작하도록 설계되어 있기 때문이다. 즉 __iter__와 __next__를 구현하고 있다:
    • __iter__(): 파일 객체 자체를 반환한다(즉 file.__iter__() == file).
    • __next__(): 파일에서 다음 라인을 읽어 반환한다. 파일의 끝에 도달하면 StopIteration 익셉션을 발생시킨다.

for line in read_large_file("large_file.txt")는 제너레이터를 통해 파일의 각 라인을 한번에 하나씩 가져온다. 이 방식은 한 번에 파일 전체를 메모리에 로드하지 않기 때문에 메모리 사용량이 최소화된다.

즉 1개 라인씩 파일에서 읽고 처리하므로 메모리 사용량은 1개 라인씩만 증가한다. 따라서 파일 크기와 무관하게 일정한 메모리만 사용한다.

 

이를 일반적인 파일 읽기 방식과 비교해보자.

with open("large_file.txt") as file:
    lines = file.readlines()  # 파일의 모든 내용을 메모리에 로드

위 방식은 파일의 모든 라인을 메모리에 로드하므로, 큰 파일의 경우 메모리 부족 문제가 발생할 수 있다.

 

2. 무한 제너레이터

무한 루프를 제너레이터로 구현하여 메모리 소모 없이 값을 끝없이 생성할 수 있다.

무한루프가 사용되므로, 제너레이터를 사용하는 쪽에서는 특정 조건을 달성하는 경우 적절히 break문을 사용해서 반복을 중단시켜줘야 한다.

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1


gen = infinite_sequence()
for i in gen:
    if i > 10:
        break
    print(i)
0
1 
2 
3 
4 
5 
6 
7 
8 
9 
10

 

3. 제너레이터 컴프리헨션(Generator Comprehension)

제너레이터 컴프리헨션(Generator Comprehension)은 리스트 컴프리헨션이나 딕셔너리/셋 컴프리헨션과 비슷하지만, 결과를 한 번에 메모리에 저장하는 대신 필요할 때마다 요소를 생성하는 제너레이터 객체를 반환하는 표현식이다. 메모리 사용량을 줄이고 효율적으로 데이터 처리를 할 수 있는 장점을 가진다.

문법

(expression for item in iterable if condition)

 

  • expression: 각 요소에 적용할 연산
  • item: 반복 가능한 객체(iterable)에서 가져온 각 요소
  • iterable: 반복 가능한 객체
  • condition: 선택적으로 요소를 필터링하는 조건

자칫 "튜플 컴프리헨션"으로 오해할 수 있으니 조심하자.

특징

  • ( )를 사용하여 정의하며, lazy evaluation(지연평가: 필요할 때 계산한다는 뜻)을 수행한다.
  • 리스트 컴프리헨션 [ ]과 비슷하지만 결과는 리스트 대신 제너레이터 객체로 반환된다.

예시

 

gen = (x for x in range(1000) if x % 3 == 0 or x % 5 == 0)
print(gen)

result = sum(gen)
print(result)  # 0부터 999까지의 3 또는 5의 배수 합
<generator object <genexpr> at 0x0000022AA625F1D0>
233168

gen은 제너레이터 컴프리헨션을 통해 도출된 제너레이터이다. sum과 같은 소비 함수에 제너레이터 gen을 사용할 수 있다.

 

 

리스트 컴프리헤션 vs 제너레이터 컴프리헨션

numbers = [x ** 2 for x in range(10**6)]  # 모든 데이터를 메모리에 저장

 

numbers = (x ** 2 for x in range(10**6))  # 필요한 데이터만 생성

 

다양한 상황에서의 제너레이터 컴프리헨션 사용

제너레이터 컴프리헨션은 대용량 파일 처리, 스트리밍 데이터 처리, 파일 스트림 처리, 데이터베이 쿼리 결과 처리, 메모리 사용량 최적화 등 메모리를 절약해서 사용해야 하는 상황에서 유용하게 사용될 수 있다. 

Lazy evaluation(지연 평가) 덕분에, 데이터를 필요한 시점에만 요청할 때마다 하나씩 생성하며, 남은 값들은 여전히 계산되지 않은 상태로 요청 전까지 대기시킬 수 있는 것이다.

 

 

(Deep Dive) 제너레이터는 곧 이터레이터!

파이썬에서 이터레이터는 __iter__()__next__() 메소드를 구현한 객체이다.

제너레이터는 특별한 형태의 이터레이터로, yield를 사용해 직접 구현한 함수를 실행할 때 자동으로 이터레이터를 생성한다.

제너레이터 함수는 실행시 제너레이터 객체를 반환하며, 이 객체는 이터레이터의 성질(즉 __iter__와 __next__ 메소드)를 가지고 있다.

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()

print("__iter__" in dir(gen))  # True (반복 가능한 객체)
print("__next__" in dir(gen))  # True (이터레이터)
True
True

위에서 볼 수 있듯, 제너레이터 함수 안에 __iter__와 __next__의 구현부가 보이지 않지만, 제너레이터는 자동으로 __iter__와 __next__ 메서드를 구현하고 있음을 알 수 있다.

※ 제너레이터 객체는 __iter____next__가 이미 구현된 상태로 만들어진다는 의미는, 제너레이터 내부적으로 C로 구현된 메커니즘을 통해 __iter____next__를 자동으로 제공한다는 뜻이다. 이 덕분에 제너레이터 함수는 간단하게 yield를 사용하면서도 이터레이터로 동작할 수 있는 것이다. (generator의 C 구현은 CPython에서 PyGenObject라는 C 구조체와 관련 함수 등을 분석해야 한다.)

 

def my_generator():
    yield 1
    yield 2
    yield 3


gen = my_generator()

print(gen.__iter__() is gen)  # True: 제너레이터 객체는 자기 자신을 반환
print(gen.__next__())  # 1: 첫 번째 `yield` 값
print(gen.__next__())  # 2: 두 번째 `yield` 값
print(gen.__next__())  # 3: 세 번째 `yield` 값

위 코드를 통해 다음을 확인할 수 있다:

  • 제너레이터의 __iter__() : 제너레이터 객체 자신을 반환한다. 따라서 제너레이터는 반복 가능한 객체이다.
  • 제너레이터의 __next__(): yield로 값을 반환하고, 다음 yield 문에서 멈추는 기능을 수행한다.

실제로 제너레이터에 대한 반복을 실행할 때, __iter__()와 __next__()가 under the hood에서 동작하고 있음을 알 수 있다!

반응형

+ Recent posts