https://comgu.tistory.com/entry/Python-Iteration의-원리Iterator-Protocol
앞서 파이썬에서 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 익셉션 발생
동작 과정
- yield는 값을 반환하면서 함수의 실행을 멈춘다.
- 다음 호출 시 함수는 이전에 멈췄던 위치부터 실행을 재개한다.
- 더 이상 yield 값이 없으면 StopIteration 예외가 발생한다.
제너레이터의 특징
- 효율적인 메모리 사용: 한 번에 모든 데이터를 메모리에 올리지 않고 필요할 때마다 하나씩 생성한다.
- 이터러블(Iterable)하다: for 루프와 같은 이터레이션 구조에서 사용할 수 있다.
- 상태 유지: 호출될 때마다 마지막으로 멈췄던 위치를 기억한다.
제너레이터의 활용 예시
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에서 동작하고 있음을 알 수 있다!
'Python' 카테고리의 다른 글
[Python] 데코레이터(Decorator) 3 - 클래스형 데코레이터 (0) | 2024.12.12 |
---|---|
[Python] 데코레이터(Decorator) 2 - 동적 데코레이터 (0) | 2024.12.12 |
[Python] 데코레이터(Decorator) 1 - 데코레이터, 중첩 데코레이터 (0) | 2024.12.11 |
[Python] 클로저(Closure) 함수 (0) | 2024.12.11 |
[Python] 객체의 shallow copy와 deep copy (1) | 2024.12.10 |