[Python] What - 이터레이터, 제너레이터

하쮸·2025년 10월 27일

Error, Why, What, How

목록 보기
48/63

1. 이터레이터란?

  • Python에서 반복문의 형식은 아래와 같음.
for (반복자) in (반복할 수 있는 것)
  • 위 코드에서 반복할 수 있는 것이터러블(Iterable)이라 함.
    • 즉, 이터러블(Iterable)은 내부에 있는 요소들을 하나씩 꺼낼 수 있는 객체를 의미함.
    • Ex.리스트, 딕셔너리, 문자열, 튜플 등 모두 내부에서 요소를 하나씩 꺼낼 수 있으므로 이터러블(Iterable)임.
  • 이터러블(Iterable)중에서 next()함수를 이용해 하나씩 꺼낼 수 있는 요소를 이터레이터(Iterator)라 함.
    • 이터러블 : 이터레이터를 생성할 수 있는 객체.
      • 이터러블(Iterable)은 내부 요소들을 하나씩 꺼낼 수 있는 이터레이터를 생성할 수 있는 객체.
    • 이터레이터 : 값을 하나씩 꺼낼 수 있는 객체.
      • 즉, 이터러블은 반복 가능한 설계도, 이터레이터는 그 설계도에서 실제로 꺼내주는 기계라 생각하면 됨.
      • 이터레이터는 next() 함수 호출 시 계속 그다음 값을 반환하는 객체.
        • 더이상 반환할 값이 없는데 next() 호출시 StopIteration 예외가 발생함.
  • 이터레이터는 반복문의 매개변수로 전달할 수 있고 next()함수를 이용해 내부의 요소를 하나씩 꺼낼 수도 있음.
    • for 반복문의 매개변수에 넣으면 반복할 때마다 next() 함수를 사용해서 요소를 하나씩 꺼내는 것.

  • 즉, 이터러블(Iterable)은 반복문에서 사용할 수 있는 객체로서 내부 요소들을 하나씩 반환하는 이터레이터를 생성할 수 있는 객체를 의미함.

    • 리스트, 튜플, 문자열, 딕셔너리 등은 모두 이터러블임.
  • 이터레이터(Iterator)는 next() 함수를 이용해 요소를 하나씩 반환하는 객체이며
    더 이상 반환할 값이 없을 경우 StopIteration 예외를 발생시킴.

  • for문은 내부적으로 이터러블에서 이터레이터를 생성하고
    반복할 때마다 next()를 호출하여 요소를 하나씩 꺼냄.


1-1. Ex.


1-1-1. reversed()는 왜 이터레이터를 반환할까?

list_a = [1, 2, 3, 4, 5]
list_a_reversed = reversed(list_a)

print("reversed() 사용.")
print("reversed(list_a) : ", list_a_reversed)
print("list(reversed(list_a)) : ", list(list_a_reversed))
print()

print("reversed()와 반복문")
print("for i in reversed(list_a):")
for i in reversed(list_a):
    print("-", i)
-- 실행 결과 --

reversed() 사용.
reversed(list_a) :  <list_reverseiterator object at 0x000001C7C125B9D0>
list(reversed(list_a)) :  [5, 4, 3, 2, 1]

reversed()와 반복문
for i in reversed(list_a):
- 5
- 4
- 3
- 2
- 1
  • numbers : 이터러블.
    • reversed(numbers) : 역방향으로 순회할 수 있는 이터레이터.
  • 리스트에서 요소의 순서를 뒤집고 싶을 때 reversed()함수를 사용.
    • reversed()이터레이터(Iterator)를 반환함.
    • Why? 왜 reversed() 함수는 리스트를 바로 반환하는 게 아니라 이터레이터를 반환할까?
      • 메모리의 효율성을 위해서.
      • 만약 수십만 개의 요소가 들어있는 리스트가 있을 경우
        이를 복제한 뒤 뒤집어서 리턴하는 것보다
        기존에 있던 리스트를 활용해서 작업하는 것이 훨씬 효율적이라고 판단하기 때문.
  • 지연 평가(lazy evaluation)
    • 이터레이터는 필요할 때만 값을 하나씩 생성하기 때문에 메모리를 거의 쓰지 않음.
nums = range(1_000_000_000)
rev = reversed(nums)
  • nums rev도 실제로 모든 숫자를 메모리에 저장하지 않음.
    • 필요할 때만 꺼냄.
  • 만약 reversed()가 이터러블 전체를 반환했다면 모든 원소를 복사해서 새 리스트를 만들었기 때문에 비효율적임.
    • 그래서 reversed()는 이터레이터 객체를 반환함.
      (필요한 순간에 하나씩 꺼낼 수 있도록.)

1-1-2. 이터레이터를 다 사용하면 어떻게 될까?

numbers = [1, 2, 3, 4, 5, 6]
reversed_num = reversed(numbers)

print("reversed_num : ", reversed_num)
print("첫 번째 : ", next(reversed_num))
print("두 번째 : ", next(reversed_num))
print("세 번째 : ", next(reversed_num))
print("네 번째 : ", next(reversed_num))
print("다섯 번째 : ", next(reversed_num))
print("여섯 번째 : ", next(reversed_num))
-- 실행 결과 --

reversed_num :  <list_reverseiterator object at 0x000001A960ABBA60>
첫 번째 :  6
두 번째 :  5
세 번째 :  4
네 번째 :  3
다섯 번째 :  2
여섯 번째 :  1
  • reversed()의 반환값이 이터레이터인 것을 확인.
  • next()를 통해 이터레이터의 값을 하나씩 꺼내서 출력.
numbers = [1, 2, 3, 4, 5, 6]
reversed_num = reversed(numbers)

print("reversed_num : ", reversed_num)
print("첫 번째 : ", next(reversed_num))
print("두 번째 : ", next(reversed_num))
print("세 번째 : ", next(reversed_num))
print("네 번째 : ", next(reversed_num))
print("다섯 번째 : ", next(reversed_num))
print("여섯 번째 : ", next(reversed_num))

print()
for i in reversed_num:
    print(f'i = {i}')

  • next()를 이용해서 이터레이터의 값들을 전부 다 꺼냈을 때 다시 출력할 수 있을까?
-- 실행 결과 --

reversed_num :  <list_reverseiterator object at 0x000001ADB318BA60>
첫 번째 :  6
두 번째 :  5
세 번째 :  4
네 번째 :  3
다섯 번째 :  2
여섯 번째 :  1
----
  • 위 실행 결과를 보면 알겠지만 출력되지않음.
    • next()로 그 값을 한 번 읽으면 그 값을 다시는 읽을 수 없음.
      • 이터레이터의 모든 요소를 다 소비(consumed) 했기 때문.
    • 즉, 더 이상 반환할 값이 없기 때문에 아무것도 출력되지않음.

  • 이터레이터의 값들을 전부 다 꺼냈을 때 다시 출력하려면
    • 아래처럼 다시 새로 생성해야됨.
numbers = [1, 2, 3, 4, 5, 6]
reversed_num = reversed(numbers)

print("reversed_num : ", reversed_num)
print("첫 번째 : ", next(reversed_num))
print("두 번째 : ", next(reversed_num))
print("세 번째 : ", next(reversed_num))
print("네 번째 : ", next(reversed_num))
print("다섯 번째 : ", next(reversed_num))
print("여섯 번째 : ", next(reversed_num))

print('----')
reversed_num = reversed(numbers)	# 새로 생성.
for i in reversed_num:
    print(f'i = {i}')
-- 실행 결과 --

reversed_num :  <list_reverseiterator object at 0x000001A974A0BA60>
첫 번째 :  6
두 번째 :  5
세 번째 :  4
네 번째 :  3
다섯 번째 :  2
여섯 번째 :  1
----
i = 6
i = 5
i = 4
i = 3
i = 2
i = 1

1-1-3. 리스트는 이터레이터일까?

list_a = [1, 2, 3]
next(list_a)
-- 실행 결과 --

Traceback (most recent call last):
  File "c:\Python-study\08\tmp.py", line 2, in <module>
    next(list_a)
TypeError: 'list' object is not an iterator
  • 리스트를 사용해서 next() 함수를 호출하니 리스트는 이터레이터 객체가 아니다라는 에러가 발생했음.
    • 즉, 반복 가능하다고 해서 이터레이터는 아님.
  • But 반복 가능하다면 아래와 같이 iter()함수를 이용해서 이터레이터로 만들 수 있음.
list_a = [1, 2, 3]

iterator_a = iter(list_a)
print(type(iterator_a))
-- 실행 결과 --

<class 'list_iterator'>

list_a = [1, 2, 3]

iterator_a = iter(list_a)

print(next(iterator_a))
print(next(iterator_a))
print(next(iterator_a))
print(next(iterator_a))
-- 실행 결과 --

1
2
3
Traceback (most recent call last):
  File "c:\Python-study\08\tmp.py", line 9, in <module>
    print(next(iterator_a))
          ^^^^^^^^^^^^^^^^
StopIteration
  • next() 함수를 호출할 때마다 이터레이터 객체의 요소를 차례대로 반환하는 것을 확인.
    • 더 이상 반환할 값이 없다면 StopIteration 예외를 발생시킴.

1-2. 직접 이터레이터 만들기.

class MyItertor:
    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 = MyItertor([1,2,3])
for item in i:
    print(item)
-- 실행 결과 --

1
2
3
  • 이터레이터는 클래스에 __iter____next__ 이렇게 총 두 개의 메서드를 구현하면 만들 수 있음.
  • MyIterator 클래스에는 이터레이터 객체를 생성하고자 __iter__ 메서드와 __next__ 메서드를 구현하였음.
    • __iter__ 메서드는 이터레이터 객체를 반환하는 메서드이므로 MyIterator 클래스에 의해 생성되는 객체를 의미하는 self를 반환.
    • __next__ 메서드는 next() 함수 호출 시 수행되므로 MyIterator 객체 생성 시 전달한 데이터를 하나씩 반환하도록 하고 더 이상 반환할 값이 없으면 StopIteration 예외를 발생.

  • 데이터를 역순으로 출력하는 ReverseIterator 클래스.
class ReverseItertor:
    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 = ReverseItertor([1,2,3])
for item in i:
    print(item)
-- 실행 결과 --

3
2
1

2. 제너레이터란?

  • 제너레이터(Generator)는 데이터가 필요할 때마다 하나씩 만들어 내는 객체.
    • 리스트(list): 모든 데이터를 즉시 메모리에 저장함.
      • 상대적으로 속도가 빠를지라도 메모리 사용량이 큼.
    • 제너레이터(generator): 요청이 있을 때마다 하나씩 생성함.
      • 상대적으로 속도가 느릴지라도 메모리 사용량이 적음.
  • 제너레이터(Generator)는 이터레이터(Iterator)를 직접 만들 때 사용하는 코드.
    • 즉, 모든 제너레이터는 이터레이터를 만드므로 제너레이터 객체는 이터레이터라 할 수 있음.
  • 함수 내부에 yield 키워드를 사용하면 해당 함수는 제너레이터 함수가 되고 일반 함수와 다르게 함수를 호출해도 함수 내부의 코드가 실행되지 않음.
  • 제너레이터 객체는 next()함수를 사용해서 함수 내부의 코드를 실행함.
    • yield까지만 실행하고 next() 함수의 리턴값으로 yield 뒤에 입력되어 있는 값이 출력됨.
  • 또한 제너레이터 객체는 함수의 코드를 조금씩 실행할 때 사용함.
    • 메모리의 효율성을 위해서.
  • 제너레이터는 한 번 쓰면 없어지는 일회용임.
    • 전체를 한 번 다 순회한 뒤에는 다시 사용할 수 없고 필요하면 새로 생성해야함.

2-1. Ex.


2-1-1. 제너레이터 함수는 제너레이터 객체를 리턴.

def generator_test():
    print("함수 호출")
    yield "test"

print("A 통과")
generator_test()

print("B 통과")
generator_test()
print(generator_test())     # 제너레이터 함수는 제너레이터를 리턴함.
-- 실행 결과 --

A 통과
B 통과
<generator object generator_test at 0x000001FC1E0C4B80>
  • generator_test() 함수를 호출했지만 내부 코드는 실행되지 않았음.
  • print()를 이용해서 generator_test()를 출력해보면
    • generator object generator_test, 즉 제너레이터 함수는 제너레이터 객체를 리턴함.

2-1-2. next() 호출시 yield가 없다면 StopIteration예외가 발생.

def generator_test():
    print("A 통과")
    yield "A"
    print("B 통과")
    yield "B"
    print("C 통과")
    # yield "C" 없음.

result = generator_test()

print("D 통과")
print(next(result))
print('----------')
print("E 통과")
print(next(result))
print('----------')
print("F 통과")
print(next(result))
print('----------')
-- 실행 결과 --

D 통과
A 통과
A
----------
E 통과
B 통과
B
----------
F 통과
C 통과
Traceback (most recent call last):
  File "c:\Python-study\05\generator_next.py", line 18, in <module>
    print(next(result))
          ^^^^^^^^^^^^
StopIteration
  • next() 함수를 호출한 다음 yield를 만나지 못한채로 함수가 끝나버리면 StopIteration예외가 발생함.

2-1-3. 제너레이터가 yield를 만나면 값을 반환하되 현재상태를 기억함.

def generator_ex():
    yield '1'
    yield '2'
    yield '3'

generator_var = generator_ex()
print(type(generator_var))
print(next(generator_var))
print(next(generator_var))
print(next(generator_var))
print(next(generator_var))
-- 실행 결과 --

<class 'generator'>
1
2
3
Traceback (most recent call last):
  File "c:\Python-study\05\generator_ex.py", line 11, in <module>
    print(next(generator_var))
          ^^^^^^^^^^^^^^^^^^^
StopIteration
  • 제너레이터 객체를 통해 next() 함수를 호출하면 generator_ex() 함수의 첫 번째 yield 문에 따라 'a' 값을 반환함.
    • 여기서 신기한 점은 제너레이터는 yield라는 문장을 만나면 그 값을 반환하되 현재의 상태를 그대로 기억함.
      (마치 음악을 재생하다가 일시 정지 버튼으로 멈춘 것과 비슷)

2-1-4. 제너레이터는 일회용임.

def gen():
    yield 10
    yield 20
    yield 30

g = gen()

for x in g:
    print(x)

print("다시 반복")
for x in g:
    print(x)
- 출력 결과 -

10
20
30
다시 반복

2-1-5. 제너레이터와 리스트의 차이점.

def gen():
    yield 10
    yield 20
    yield 30

lst = [10, 20, 30]		# 리스트
g = (x for x in lst)    # 제너레이터

print('리스트1: ', list(lst))
print('리스트2: ', list(lst))

print('제너레이터1: ', list(g))
print('제너레이터2: ', list(g))
- 출력 결과 -

리스트1:  [10, 20, 30]
리스트2:  [10, 20, 30]
제너레이터1:  [10, 20, 30]
제너레이터2:  []

3. 참고.

  • 혼자 공부하는 파이썬
  • 점프 투 파이썬
profile
Every cloud has a silver lining.

0개의 댓글