점프 투 파이썬 DAY16(이터레이터와 제너레이터)

정지범·6일 전

점프 투 파이썬

목록 보기
31/37

07-3 이터레이터와 제너레이터

목차

  • 1. 반복 가능한 객체란?
  • 2. 이터레이터란?
  • 3. iter()와 next()
  • 4. 이터레이터 직접 만들기
  • 5. 제너레이터란?
  • 6. yield 동작 방식
  • 7. 제너레이터 표현식
  • 8. 이터레이터와 제너레이터 비교
  • 9. 제너레이터 활용하기

1. 반복 가능한 객체란?

다음과 같은 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 객체이다.


2. 이터레이터란?

이터레이터(Iterator) 는

next()를 이용해 값을 하나씩 꺼낼 수 있는 객체

이다.

여기서 중요한 점이 있다.

반복 가능한 객체라고 해서 모두 이터레이터인 것은 아니다.

다음 코드를 실행해 보자.

a = [1, 2, 3]

next(a)

오류가 발생한다.

TypeError: 'list' object is not an iterator

리스트는 for문으로 반복할 수 있기 때문에 iterable이지만, next()를 바로 사용할 수 있는 iterator는 아니다.


쉽게 비유하면

다음처럼 생각하면 이해하기 쉽다.

리스트 → 책
이터레이터 → 책갈피

책은 읽을 수 있는 대상이다.

하지만 현재 어느 페이지를 읽고 있는지 기억하면서 한 장씩 넘겨 주는 역할은 책갈피가 한다.

마찬가지로

Iterable
   ↓
반복할 수 있는 데이터

Iterator
   ↓
현재 위치를 기억하면서
값을 하나씩 꺼내 주는 객체

라고 생각할 수 있다.


3. iter() 함수

리스트와 같은 iterable 객체를 iterator로 만들 때

iter()

함수를 사용한다.

a = [1, 2, 3]

ia = iter(a)

자료형을 확인해 보자.

print(type(ia))

결과

<class 'list_iterator'>

이제 ia는 리스트가 아니라 리스트 이터레이터이다.


4. next() 함수

이터레이터에서는 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

이다.


5. 더 이상 값이 없다면?

다시 next()를 실행하면 어떻게 될까?

next(ia)

결과

StopIteration

이터레이터에서 가져올 값이 더 이상 없으면

StopIteration

예외가 발생한다.

즉

next()
  ↓
다음 값 존재?
  ↓
Yes → 값 반환

No → StopIteration

과 같이 동작한다.


6. for문과 이터레이터

실제로는 우리가 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을 처리할 필요가 없다.


7. 이터레이터는 한 번 사용하면 끝난다

다음 코드를 살펴보자.

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)

8. Iterable과 Iterator 정리

구분IterableIterator
의미반복 가능한 객체값을 하나씩 꺼내는 객체
for문가능가능
next()바로 사용 불가능할 수 있음가능
예list, tuple, strlist_iterator
변환iter() 사용이미 iterator

핵심 관계는

Iterable
   ↓
 iter()
   ↓
Iterator
   ↓
 next()
   ↓
값 하나씩 반환

이다.


9. 이터레이터 직접 만들기

이번에는 클래스를 이용하여 직접 이터레이터를 만들어 보자.

이터레이터를 만들기 위해 중요한 메서드는 두 가지이다.

__iter__()
__next__()

iter()

def __iter__(self):
    return self

이터레이터 객체 자신을 반환한다.

iter()가 호출될 때 사용된다.


next()

def __next__(self):

다음 값을 하나씩 반환한다.

더 이상 반환할 값이 없다면

raise StopIteration

을 사용한다.


10. MyIterator 만들기

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

11. init 이해하기

객체를 만들면

i = MyIterator([1, 2, 3])

다음 값들이 저장된다.

self.data = [1, 2, 3]
self.position = 0

여기서

self.position

은 현재 어디까지 읽었는지를 기억하는 변수이다.

처음에는

position = 0

이다.


12. next 동작 과정

첫 번째 next()를 생각해 보자.

result = self.data[self.position]

현재

self.position = 0

이므로

self.data[0]

즉

1

을 가져온다.

그리고

self.position += 1

을 실행한다.

따라서

position = 1

이 된다.


다음 next()

이번에는

self.data[1]

이므로

2

를 반환한다.

이 과정을 반복하면

position반환 값다음 position
011
122
233

마지막에는

self.position >= len(self.data)

조건이 참이 된다.

3 >= 3
→ True

따라서

raise StopIteration

이 실행된다.


13. 역순 이터레이터 만들기

이번에는 데이터를 반대로 가져오는 이터레이터를 만들어 보자.

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

순서로 가져오게 된다.


14. 제너레이터란?

클래스로 이터레이터를 만들려면

__iter__()
__next__()

를 직접 구현해야 했다.

하지만 제너레이터(Generator) 를 사용하면 훨씬 간단하게 이터레이터를 만들 수 있다.

제너레이터는 이터레이터를 쉽게 만들 수 있도록 해 주는 함수이다.

제너레이터의 가장 중요한 키워드는

yield

이다.


15. yield 사용하기

다음 함수를 살펴보자.

def mygen():
    yield "a"
    yield "b"
    yield "c"

일반적인 함수에서는

return

을 사용하지만 제너레이터에서는

yield

를 사용한다.


제너레이터 객체 만들기

g = mygen()

자료형을 확인하면

print(type(g))

결과

<class 'generator'>

가 된다.


16. 제너레이터에서 next() 사용하기

print(next(g))

결과

a

다시 실행하면

print(next(g))

결과

b

다시 실행하면

print(next(g))

결과

c

한 번 더 실행하면

next(g)
StopIteration

이 발생한다.

즉 제너레이터 역시 이터레이터이기 때문에 next()를 사용할 수 있다.


17. yield는 return과 무엇이 다를까?

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" 반환
 ↓
다시 일시 정지

18. 음악 플레이어처럼 생각하기

yield는 음악 플레이어의 일시 정지와 비슷하다.

▶ 재생

코드 실행
   ↓
yield
   ↓
⏸ 일시 정지

next()
   ↓
▶ 다시 재생
   ↓
다음 yield
   ↓
⏸ 다시 정지

즉 제너레이터는 실행 상태를 기억한다.


19. 제너레이터 예제

다음과 같이 숫자의 제곱을 하나씩 반환하는 제너레이터를 만들 수 있다.

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 반환

처럼 필요할 때마다 하나씩 계산한다.


20. 제너레이터 표현식

앞의 코드는 다음과 같다.

def mygen():

    for i in range(1, 1000):
        yield i * i

파이썬에서는 이것을 더 간단하게 만들 수도 있다.

gen = (i * i for i in range(1, 1000))

이것을

제너레이터 표현식(Generator Expression)

이라고 한다.


21. 리스트 컴프리헨션과 비교

리스트 컴프리헨션은

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'>

이다.


22. 클래스 이터레이터와 제너레이터

동일한 기능을 클래스로 만들 수도 있다.

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))

한 줄이면 된다.

간단한 반복 기능이라면 제너레이터를 이용하는 것이 훨씬 간결하다.


23. 제너레이터를 사용하는 이유

제너레이터의 가장 큰 장점 중 하나는

필요한 값만 그때그때 계산한다는 것

이다.

예를 들어 시간이 오래 걸리는 함수가 있다고 해보자.

import time

def longtime_job():

    print("job start")

    time.sleep(1)

    return "done"

한 번 실행하는 데 약 1초가 걸린다고 가정한다.


24. 리스트 컴프리헨션 사용

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 실행
 ↓
모든 결과 저장
 ↓
첫 번째 결과 사용

과 같다.


25. 제너레이터 표현식 사용

이번에는 제너레이터로 만들어 보자.

gen_job = (longtime_job() for i in range(5))

print(next(gen_job))

결과

job start
done

gen_job을 만드는 순간에는 longtime_job()을 실행하지 않는다.

실제로

next(gen_job)

을 호출하는 순간 첫 번째 작업만 실행한다.

제너레이터 생성
 ↓
아무 작업도 하지 않음

next()
 ↓
첫 번째 작업 실행
 ↓
결과 반환
 ↓
정지

따라서 첫 번째 값만 필요하다면 나머지 작업은 실행하지 않아도 된다.


26. Lazy Evaluation

제너레이터처럼

필요한 순간에 값을 계산하는 방식

을

느긋한 계산법(Lazy Evaluation)

이라고 한다.

반대로 리스트는 모든 결과를 먼저 만들어 놓는다.

리스트

값1 생성
값2 생성
값3 생성
값4 생성
값5 생성
 ↓
모두 메모리에 저장

제너레이터는

Generator

next()
 ↓
값1 생성

next()
 ↓
값2 생성

next()
 ↓
값3 생성

처럼 요청받을 때 값을 생성한다.


27. 메모리 차이

예를 들어 매우 많은 데이터를 처리한다고 생각해 보자.

리스트

numbers = [i for i in range(10000000)]

는 많은 값을 한꺼번에 만들어 메모리에 저장한다.

반면

numbers = (i for i in range(10000000))

제너레이터는 모든 값을 미리 저장하지 않는다.

필요할 때

next(numbers)

를 호출해 하나씩 만든다.

따라서 대용량 데이터를 처리할 때 메모리를 효율적으로 사용할 수 있다.


28. 이터레이터와 제너레이터 관계

둘의 관계를 정리하면 다음과 같다.

Iterable
   ↓
iter()
   ↓
Iterator

제너레이터 역시

Generator
   ↓
Iterator의 한 종류

라고 생각할 수 있다.

즉 제너레이터에는

next()

를 바로 사용할 수 있다.


29. 이터레이터와 제너레이터 비교

구분이터레이터제너레이터
목적값을 하나씩 반환값을 하나씩 반환
next()사용 가능사용 가능
종료StopIterationStopIteration
만드는 방법__iter__, __next__yield
상태 기억OO
구현비교적 복잡간단
대표 키워드iter, nextyield

쉽게 말하면

이터레이터

직접 구조를 구현
__iter__
__next__

반면

제너레이터

yield 사용
 ↓
파이썬이 이터레이터 구조를 자동으로 만들어 줌

이라고 생각하면 된다.


30. 전체 흐름 정리

이번 장의 흐름을 한 번에 정리하면 다음과 같다.

리스트 [1, 2, 3]
      ↓
반복 가능
      ↓
Iterable
      ↓
iter()
      ↓
Iterator
      ↓
next()
      ↓
1
      ↓
next()
      ↓
2
      ↓
next()
      ↓
3
      ↓
StopIteration

그리고 직접 이터레이터를 간단하게 만드는 방법이

Generator

이다.

일반 함수
   ↓
yield 사용
   ↓
Generator 함수
   ↓
호출
   ↓
Generator 객체
   ↓
next()
   ↓
yield 값 반환
   ↓
실행 위치 기억
   ↓
다음 next()
   ↓
중단한 위치부터 다시 실행

📌 핵심 정리

Iterable

for문으로 반복할 수 있는 객체이다.

[1, 2, 3]
"Python"
(1, 2, 3)

Iterator

next()를 이용해 값을 하나씩 가져올 수 있는 객체이다.

a = [1, 2, 3]

ia = iter(a)

print(next(ia))
1

iter()

Iterable 객체를 Iterator로 만든다.

ia = iter([1, 2, 3])

next()

이터레이터의 다음 값을 가져온다.

next(ia)

더 이상 값이 없다면

StopIteration

이 발생한다.


이터레이터 클래스

직접 이터레이터를 만들 때는

__iter__()
__next__()

를 구현한다.

class MyIterator:

    def __iter__(self):
        return self

    def __next__(self):
        ...

Generator

yield를 이용해 간단하게 이터레이터를 만들 수 있다.

def mygen():
    yield 1
    yield 2
    yield 3

yield

값을 반환하고 함수 실행을 완전히 종료하는 것이 아니라 현재 위치에서 일시 정지한다.

실행
 ↓
yield
 ↓
값 반환 + 일시 정지
 ↓
next()
 ↓
중단한 위치부터 다시 실행

제너레이터 표현식

gen = (i * i for i in range(1, 1000))

리스트 컴프리헨션

[i * i for i in range(1, 1000)]

과 비슷하지만 [] 대신 ()를 사용한다.


Lazy Evaluation

제너레이터는 모든 값을 미리 계산하지 않는다.

필요할 때
 ↓
하나 계산
 ↓
반환
 ↓
정지

따라서

  • 대용량 데이터 처리
  • 메모리 절약
  • 시간이 오래 걸리는 작업
  • 필요한 결과만 가져오는 작업

등에서 유용하다.


💡 가장 중요한 차이

List

[1, 2, 3, 4, 5]

모든 값을
미리 만들어 저장
Generator

next() → 1
next() → 2
next() → 3

필요할 때
하나씩 생성

그리고 가장 중요한 흐름은

Iterable
   ↓
iter()
   ↓
Iterator
   ↓
next()
   ↓
값 하나씩 반환

이다.

제너레이터는 이 Iterator를 직접 복잡하게 만들지 않고

yield

하나로 쉽게 만들 수 있게 해 준다.

Iterable은 반복할 수 있는 대상, Iterator는 그 대상에서 값을 하나씩 꺼내는 객체, Generator는 그런 Iterator를 yield로 쉽게 만드는 방법이라고 이해하면 된다.

profile
성실하자:)

0개의 댓글