목차
- 1. 반복 가능한 객체란?
- 2. 이터레이터란?
- 3. iter()와 next()
- 4. 이터레이터 직접 만들기
- 5. 제너레이터란?
- 6. yield 동작 방식
- 7. 제너레이터 표현식
- 8. 이터레이터와 제너레이터 비교
- 9. 제너레이터 활용하기
다음과 같은 for문을 생각해 보자.
for a in [1, 2, 3]:
print(a)
실행 결과
1
2
3
리스트 [1, 2, 3]의 값을 하나씩 가져와 출력한다.
이처럼 for문과 같은 반복문에서 사용할 수 있는 객체를
반복 가능한 객체(Iterable)
라고 한다.
대표적인 iterable 객체에는 다음과 같은 것들이 있다.
list
tuple
string
dict
set
range
예를 들어
for char in "Python":
print(char)
문자열도 하나씩 반복할 수 있으므로 iterable 객체이다.
이터레이터(Iterator) 는
next()를 이용해 값을 하나씩 꺼낼 수 있는 객체
이다.
여기서 중요한 점이 있다.
반복 가능한 객체라고 해서 모두 이터레이터인 것은 아니다.
다음 코드를 실행해 보자.
a = [1, 2, 3]
next(a)
오류가 발생한다.
TypeError: 'list' object is not an iterator
리스트는 for문으로 반복할 수 있기 때문에 iterable이지만, next()를 바로 사용할 수 있는 iterator는 아니다.
다음처럼 생각하면 이해하기 쉽다.
리스트 → 책
이터레이터 → 책갈피
책은 읽을 수 있는 대상이다.
하지만 현재 어느 페이지를 읽고 있는지 기억하면서 한 장씩 넘겨 주는 역할은 책갈피가 한다.
마찬가지로
Iterable
↓
반복할 수 있는 데이터
Iterator
↓
현재 위치를 기억하면서
값을 하나씩 꺼내 주는 객체
라고 생각할 수 있다.
리스트와 같은 iterable 객체를 iterator로 만들 때
iter()
함수를 사용한다.
a = [1, 2, 3]
ia = iter(a)
자료형을 확인해 보자.
print(type(ia))
결과
<class 'list_iterator'>
이제 ia는 리스트가 아니라 리스트 이터레이터이다.
이터레이터에서는 next()를 사용할 수 있다.
a = [1, 2, 3]
ia = iter(a)
print(next(ia))
print(next(ia))
print(next(ia))
실행 결과
1
2
3
next()를 호출할 때마다 다음 값을 하나씩 가져온다.
동작 과정을 보면
[1, 2, 3]
↓
iter()
↓
이터레이터 생성
↓
next() → 1
↓
next() → 2
↓
next() → 3
이다.
다시 next()를 실행하면 어떻게 될까?
next(ia)
결과
StopIteration
이터레이터에서 가져올 값이 더 이상 없으면
StopIteration
예외가 발생한다.
즉
next()
↓
다음 값 존재?
↓
Yes → 값 반환
No → StopIteration
과 같이 동작한다.
실제로는 우리가 next()와 StopIteration을 직접 처리하는 경우보다 for문을 사용하는 경우가 많다.
a = [1, 2, 3]
ia = iter(a)
for i in ia:
print(i)
결과
1
2
3
for문은 내부적으로 iterator에서 값을 하나씩 가져오고, 더 이상 값이 없으면 반복을 종료한다.
쉽게 보면 다음과 같은 흐름이다.
for문 시작
↓
next()
↓
값 존재
↓
값 사용
↓
next()
↓
...
↓
StopIteration
↓
반복 종료
따라서 우리가 직접 StopIteration을 처리할 필요가 없다.
다음 코드를 살펴보자.
a = [1, 2, 3]
ia = iter(a)
for i in ia:
print(i)
결과
1
2
3
다시 한번 반복해 보자.
for i in ia:
print(i)
이번에는 아무것도 출력되지 않는다.
왜 그럴까?
이터레이터는 현재 위치를 기억하면서 앞으로만 이동하기 때문이다.
처음
1 → 2 → 3
↑
현재 위치
next()
1 → 2 → 3
↑
next()
1 → 2 → 3
↑
모두 사용
1 → 2 → 3 → 끝
이미 마지막까지 이동했기 때문에 다시 처음으로 돌아가지 않는다.
다시 사용하려면 새로운 이터레이터를 만들어야 한다.
ia = iter(a)
| 구분 | Iterable | Iterator |
|---|---|---|
| 의미 | 반복 가능한 객체 | 값을 하나씩 꺼내는 객체 |
for문 | 가능 | 가능 |
next() | 바로 사용 불가능할 수 있음 | 가능 |
| 예 | list, tuple, str | list_iterator |
| 변환 | iter() 사용 | 이미 iterator |
핵심 관계는
Iterable
↓
iter()
↓
Iterator
↓
next()
↓
값 하나씩 반환
이다.
이번에는 클래스를 이용하여 직접 이터레이터를 만들어 보자.
이터레이터를 만들기 위해 중요한 메서드는 두 가지이다.
__iter__()
__next__()
def __iter__(self):
return self
이터레이터 객체 자신을 반환한다.
iter()가 호출될 때 사용된다.
def __next__(self):
다음 값을 하나씩 반환한다.
더 이상 반환할 값이 없다면
raise StopIteration
을 사용한다.
class MyIterator:
def __init__(self, data):
self.data = data
self.position = 0
def __iter__(self):
return self
def __next__(self):
if self.position >= len(self.data):
raise StopIteration
result = self.data[self.position]
self.position += 1
return result
사용
i = MyIterator([1, 2, 3])
for item in i:
print(item)
결과
1
2
3
객체를 만들면
i = MyIterator([1, 2, 3])
다음 값들이 저장된다.
self.data = [1, 2, 3]
self.position = 0
여기서
self.position
은 현재 어디까지 읽었는지를 기억하는 변수이다.
처음에는
position = 0
이다.
첫 번째 next()를 생각해 보자.
result = self.data[self.position]
현재
self.position = 0
이므로
self.data[0]
즉
1
을 가져온다.
그리고
self.position += 1
을 실행한다.
따라서
position = 1
이 된다.
이번에는
self.data[1]
이므로
2
를 반환한다.
이 과정을 반복하면
| position | 반환 값 | 다음 position |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 2 | 2 |
| 2 | 3 | 3 |
마지막에는
self.position >= len(self.data)
조건이 참이 된다.
3 >= 3
→ True
따라서
raise StopIteration
이 실행된다.
이번에는 데이터를 반대로 가져오는 이터레이터를 만들어 보자.
class ReverseIterator:
def __init__(self, data):
self.data = data
self.position = len(self.data) - 1
def __iter__(self):
return self
def __next__(self):
if self.position < 0:
raise StopIteration
result = self.data[self.position]
self.position -= 1
return result
사용
i = ReverseIterator([1, 2, 3])
for item in i:
print(item)
결과
3
2
1
이번에는 시작 위치가
len(self.data) - 1
이다.
데이터가
[1, 2, 3]
이라면
len = 3
3 - 1
= 2
이므로
self.data[2]
부터 시작한다.
즉
3 → 2 → 1
순서로 가져오게 된다.
클래스로 이터레이터를 만들려면
__iter__()
__next__()
를 직접 구현해야 했다.
하지만 제너레이터(Generator) 를 사용하면 훨씬 간단하게 이터레이터를 만들 수 있다.
제너레이터는 이터레이터를 쉽게 만들 수 있도록 해 주는 함수이다.
제너레이터의 가장 중요한 키워드는
yield
이다.
다음 함수를 살펴보자.
def mygen():
yield "a"
yield "b"
yield "c"
일반적인 함수에서는
return
을 사용하지만 제너레이터에서는
yield
를 사용한다.
g = mygen()
자료형을 확인하면
print(type(g))
결과
<class 'generator'>
가 된다.
print(next(g))
결과
a
다시 실행하면
print(next(g))
결과
b
다시 실행하면
print(next(g))
결과
c
한 번 더 실행하면
next(g)
StopIteration
이 발생한다.
즉 제너레이터 역시 이터레이터이기 때문에 next()를 사용할 수 있다.
return은 값을 반환하면 함수가 완전히 종료된다.
def test():
return 1
함수 실행
↓
return
↓
값 반환
↓
함수 종료
하지만 yield는 조금 다르다.
def mygen():
yield "a"
yield "b"
yield "c"
첫 번째 next()를 호출하면
함수 시작
↓
yield "a"
↓
"a" 반환
↓
함수 일시 정지
한다.
다음 next()를 호출하면 처음부터 다시 실행하는 것이 아니라 멈췄던 위치부터 다시 실행한다.
다음 next()
↓
이전 yield 다음부터 시작
↓
yield "b"
↓
"b" 반환
↓
다시 일시 정지
yield는 음악 플레이어의 일시 정지와 비슷하다.
▶ 재생
코드 실행
↓
yield
↓
⏸ 일시 정지
next()
↓
▶ 다시 재생
↓
다음 yield
↓
⏸ 다시 정지
즉 제너레이터는 실행 상태를 기억한다.
다음과 같이 숫자의 제곱을 하나씩 반환하는 제너레이터를 만들 수 있다.
def mygen():
for i in range(1, 1000):
result = i * i
yield result
제너레이터 객체를 만든다.
gen = mygen()
그리고
print(next(gen))
print(next(gen))
print(next(gen))
을 실행하면
1
4
9
가 출력된다.
중요한 점은 1~999의 제곱을 한꺼번에 모두 만드는 것이 아니라는 것이다.
next()
↓
1² 계산
↓
1 반환
↓
정지
next()
↓
2² 계산
↓
4 반환
↓
정지
next()
↓
3² 계산
↓
9 반환
처럼 필요할 때마다 하나씩 계산한다.
앞의 코드는 다음과 같다.
def mygen():
for i in range(1, 1000):
yield i * i
파이썬에서는 이것을 더 간단하게 만들 수도 있다.
gen = (i * i for i in range(1, 1000))
이것을
제너레이터 표현식(Generator Expression)
이라고 한다.
리스트 컴프리헨션은
numbers = [i * i for i in range(1, 1000)]
처럼 []를 사용한다.
제너레이터 표현식은
numbers = (i * i for i in range(1, 1000))
처럼 ()를 사용한다.
| 구분 | 리스트 컴프리헨션 | 제너레이터 표현식 |
|---|---|---|
| 괄호 | [ ] | ( ) |
| 결과 | 리스트 | 제너레이터 |
| 값 생성 | 모두 생성 | 필요할 때 생성 |
| 메모리 | 상대적으로 많이 사용 | 효율적 |
(i * i for i in range(1, 1000))
에서 소괄호 ()를 사용했다고 해서 튜플이 아니다.
자료형을 확인하면
gen = (i * i for i in range(1, 1000))
print(type(gen))
결과는
<class 'generator'>
이다.
동일한 기능을 클래스로 만들 수도 있다.
class MyIterator:
def __init__(self):
self.data = 1
def __iter__(self):
return self
def __next__(self):
if self.data >= 1000:
raise StopIteration
result = self.data * self.data
self.data += 1
return result
하지만 같은 작업을 제너레이터 표현식으로 만들면
gen = (i * i for i in range(1, 1000))
한 줄이면 된다.
간단한 반복 기능이라면 제너레이터를 이용하는 것이 훨씬 간결하다.
제너레이터의 가장 큰 장점 중 하나는
필요한 값만 그때그때 계산한다는 것
이다.
예를 들어 시간이 오래 걸리는 함수가 있다고 해보자.
import time
def longtime_job():
print("job start")
time.sleep(1)
return "done"
한 번 실행하는 데 약 1초가 걸린다고 가정한다.
list_job = [longtime_job() for i in range(5)]
print(list_job[0])
리스트 컴프리헨션은 리스트를 만들기 위해 longtime_job()을 5번 모두 실행한다.
job start
job start
job start
job start
job start
done
첫 번째 결과만 사용하고 싶어도 이미 모든 작업을 실행한다.
동작 과정은
리스트 생성 시작
↓
작업 1 실행
↓
작업 2 실행
↓
작업 3 실행
↓
작업 4 실행
↓
작업 5 실행
↓
모든 결과 저장
↓
첫 번째 결과 사용
과 같다.
이번에는 제너레이터로 만들어 보자.
gen_job = (longtime_job() for i in range(5))
print(next(gen_job))
결과
job start
done
gen_job을 만드는 순간에는 longtime_job()을 실행하지 않는다.
실제로
next(gen_job)
을 호출하는 순간 첫 번째 작업만 실행한다.
제너레이터 생성
↓
아무 작업도 하지 않음
next()
↓
첫 번째 작업 실행
↓
결과 반환
↓
정지
따라서 첫 번째 값만 필요하다면 나머지 작업은 실행하지 않아도 된다.
제너레이터처럼
필요한 순간에 값을 계산하는 방식
을
느긋한 계산법(Lazy Evaluation)
이라고 한다.
반대로 리스트는 모든 결과를 먼저 만들어 놓는다.
리스트
값1 생성
값2 생성
값3 생성
값4 생성
값5 생성
↓
모두 메모리에 저장
제너레이터는
Generator
next()
↓
값1 생성
next()
↓
값2 생성
next()
↓
값3 생성
처럼 요청받을 때 값을 생성한다.
예를 들어 매우 많은 데이터를 처리한다고 생각해 보자.
리스트
numbers = [i for i in range(10000000)]
는 많은 값을 한꺼번에 만들어 메모리에 저장한다.
반면
numbers = (i for i in range(10000000))
제너레이터는 모든 값을 미리 저장하지 않는다.
필요할 때
next(numbers)
를 호출해 하나씩 만든다.
따라서 대용량 데이터를 처리할 때 메모리를 효율적으로 사용할 수 있다.
둘의 관계를 정리하면 다음과 같다.
Iterable
↓
iter()
↓
Iterator
제너레이터 역시
Generator
↓
Iterator의 한 종류
라고 생각할 수 있다.
즉 제너레이터에는
next()
를 바로 사용할 수 있다.
| 구분 | 이터레이터 | 제너레이터 |
|---|---|---|
| 목적 | 값을 하나씩 반환 | 값을 하나씩 반환 |
next() | 사용 가능 | 사용 가능 |
| 종료 | StopIteration | StopIteration |
| 만드는 방법 | __iter__, __next__ | yield |
| 상태 기억 | O | O |
| 구현 | 비교적 복잡 | 간단 |
| 대표 키워드 | iter, next | yield |
쉽게 말하면
이터레이터
직접 구조를 구현
__iter__
__next__
반면
제너레이터
yield 사용
↓
파이썬이 이터레이터 구조를 자동으로 만들어 줌
이라고 생각하면 된다.
이번 장의 흐름을 한 번에 정리하면 다음과 같다.
리스트 [1, 2, 3]
↓
반복 가능
↓
Iterable
↓
iter()
↓
Iterator
↓
next()
↓
1
↓
next()
↓
2
↓
next()
↓
3
↓
StopIteration
그리고 직접 이터레이터를 간단하게 만드는 방법이
Generator
이다.
일반 함수
↓
yield 사용
↓
Generator 함수
↓
호출
↓
Generator 객체
↓
next()
↓
yield 값 반환
↓
실행 위치 기억
↓
다음 next()
↓
중단한 위치부터 다시 실행
for문으로 반복할 수 있는 객체이다.
[1, 2, 3]
"Python"
(1, 2, 3)
next()를 이용해 값을 하나씩 가져올 수 있는 객체이다.
a = [1, 2, 3]
ia = iter(a)
print(next(ia))
1
Iterable 객체를 Iterator로 만든다.
ia = iter([1, 2, 3])
이터레이터의 다음 값을 가져온다.
next(ia)
더 이상 값이 없다면
StopIteration
이 발생한다.
직접 이터레이터를 만들 때는
__iter__()
__next__()
를 구현한다.
class MyIterator:
def __iter__(self):
return self
def __next__(self):
...
yield를 이용해 간단하게 이터레이터를 만들 수 있다.
def mygen():
yield 1
yield 2
yield 3
값을 반환하고 함수 실행을 완전히 종료하는 것이 아니라 현재 위치에서 일시 정지한다.
실행
↓
yield
↓
값 반환 + 일시 정지
↓
next()
↓
중단한 위치부터 다시 실행
gen = (i * i for i in range(1, 1000))
리스트 컴프리헨션
[i * i for i in range(1, 1000)]
과 비슷하지만 [] 대신 ()를 사용한다.
제너레이터는 모든 값을 미리 계산하지 않는다.
필요할 때
↓
하나 계산
↓
반환
↓
정지
따라서
등에서 유용하다.
List
[1, 2, 3, 4, 5]
모든 값을
미리 만들어 저장
Generator
next() → 1
next() → 2
next() → 3
필요할 때
하나씩 생성
그리고 가장 중요한 흐름은
Iterable
↓
iter()
↓
Iterator
↓
next()
↓
값 하나씩 반환
이다.
제너레이터는 이 Iterator를 직접 복잡하게 만들지 않고
yield
하나로 쉽게 만들 수 있게 해 준다.
Iterable은 반복할 수 있는 대상,Iterator는 그 대상에서 값을 하나씩 꺼내는 객체,Generator는 그런 Iterator를yield로 쉽게 만드는 방법이라고 이해하면 된다.