Python & AI Tutorials Logo
Python 프로그래밍

35. 반복이 동작하는 방식: 이터러블과 이터레이터

이 책 전반에 걸쳐 여러분은 for 반복문(loop)을 사용해 리스트, 문자열, 딕셔너리, 그리고 다른 컬렉션을 반복(iteration)해 왔습니다. 여러분은 for item in my_list: 같은 코드를 수없이 작성해 봤을 것입니다. 그런데 Python이 for 반복문을 실행할 때 실제로는 내부에서 어떤 일이 일어날까요? Python은 서로 다른 타입의 컬렉션을 어떻게 순서대로 진행(step)해야 하는지 어떻게 알까요?

이 장에서는 for 반복문이 동작하게 만드는 메커니즘인 Python의 반복 프로토콜(iteration protocol)을 살펴봅니다. 여러분은 이터러블(iterable)(반복할 수 있는 객체)과 이터레이터(iterator)(실제로 값들을 하나씩 진행하며 꺼내는 객체)에 대해 배우게 됩니다. 이 둘의 차이를 이해하면 Python이 어떻게 동작하는지에 대한 지식이 깊어지고, 36장에서 제너레이터(generator)를 다루기 위한 준비가 됩니다.

35.1) 객체가 이터러블이라는 것의 의미

35.1.1) 이터러블성의 개념

이터러블(iterable)for 반복문으로 반복할 수 있는 모든 Python 객체입니다. 여기서 “반복할 수 있다”는 말은 Python이 객체로부터 항목을 한 번에 하나씩, 순서대로 가져올 수 있다는 뜻입니다.

여러분은 이미 많은 이터러블을 다뤄 봤습니다:

python
# 리스트는 이터러블입니다
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    print(num)  # Output: 1, 2, 3, 4, 5 (on separate lines)
 
# 문자열은 이터러블입니다
text = "Python"
for char in text:
    print(char)  # Output: P, y, t, h, o, n (on separate lines)
 
# 딕셔너리는 이터러블입니다(기본적으로 키를 대상으로 반복)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)

이 모든 객체들—리스트, 문자열, 딕셔너리, 튜플, 집합, range, 파일—은 Python의 iteration protocol(Python이 이들을 순회할 수 있게 하는 규칙 집합)을 지원하기 때문에 iterable입니다.

35.1.2) 객체를 이터러블로 만드는 것

객체가 이터러블이 되려면 __iter__()라는 특별한 메서드를 구현해야 합니다. 이 메서드는 이터레이터(iterator) 객체를 반환합니다. 아직 자세한 내용은 걱정하지 마세요—다음 섹션에서 이터레이터를 살펴보겠습니다.

내장 함수 iter()를 사용해 객체로부터 이터레이터를 얻어보면, 객체가 이터러블인지 확인할 수 있습니다:

python
# 객체가 이터러블인지 테스트하기
numbers = [1, 2, 3]
iterator = iter(numbers)  # 동작함 - 리스트는 이터러블입니다
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hello"
iterator = iter(text)  # 동작함 - 문자열은 이터러블입니다
print(type(iterator))  # Output: <class 'str_iterator'>
 
# 이터러블이 아닌 객체로 시도하기
value = 42
try:
    iterator = iter(value)  # 실패함 - 정수는 이터러블이 아닙니다
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'int' object is not iterable

이터러블 객체에 iter()를 호출하면 Python은 해당 객체의 __iter__() 메서드를 호출하고 이터레이터를 반환합니다. 객체에 이 메서드가 없으면 TypeError가 발생합니다.

35.1.3) 이터러블 vs 시퀀스

모든 이터러블이 시퀀스(sequence)는 아니라는 점을 이해하는 것이 중요합니다. 시퀀스는 인덱싱을 지원하고, 정해진 순서가 있는 특정한 타입의 이터러블입니다.

python
# 시퀀스는 인덱싱을 지원합니다
my_list = [10, 20, 30]
print(my_list[0])  # Output: 10
 
my_string = "Python"
print(my_string[2])  # Output: t
 
# 세트는 이터러블이지만 시퀀스가 아닙니다(인덱싱 없음, 순서 보장 없음)
my_set = {1, 2, 3}
for item in my_set:
    print(item)  # 동작함 - 세트는 이터러블입니다
 
# 하지만 인덱싱은 동작하지 않습니다
try:
    print(my_set[0])  # 실패함 - 세트는 인덱싱을 지원하지 않습니다
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'set' object is not subscriptable

핵심 구분: 모든 시퀀스(리스트, 튜플, 문자열, range)는 이터러블이지만, 모든 이터러블이 시퀀스인 것은 아닙니다. 세트와 딕셔너리는 이터러블이지만 인덱싱을 지원하지 않기 때문에 시퀀스가 아닙니다.

Python 객체

이터러블

비-이터러블

시퀀스

비-시퀀스 이터러블

리스트, 튜플, 문자열, Range

세트, 딕셔너리, 파일

숫자, None, 불리언

35.1.4) 이터러블성이 중요한 이유

이터러블성을 이해하면 다음에 도움이 됩니다:

  1. 무엇을 반복할 수 있는지 알기: 어떤 이터러블이든 for 반복문에서 동작합니다
  2. 에러 메시지 이해하기: "object is not iterable"은 for 반복문에서 사용할 수 없다는 뜻입니다
  3. 컴프리헨션(comprehension) 사용하기: 리스트/세트/딕셔너리 컴프리헨션은 어떤 이터러블과도 동작합니다
  4. 내장 함수 활용하기: sum(), max(), min(), sorted() 같은 많은 내장 함수는 어떤 이터러블이든 받을 수 있습니다
python
# 이 모든 것은 이터러블을 받기 때문에 동작합니다
numbers = [1, 2, 3, 4, 5]
print(sum(numbers))  # Output: 15
 
text = "Python"
print(max(text))  # Output: y (highest alphabetically)
 
# 세트에서도 동작합니다
unique_values = {10, 5, 20, 15}
print(sorted(unique_values))  # Output: [5, 10, 15, 20]

35.2) Python에서 흔히 보는 이터레이터(파일, range, 딕셔너리 등)

35.2.1) 이터레이터란 무엇인가

이터레이터(iterator)는 데이터 스트림을 나타내는 객체입니다. 다음 항목을 요청할 때마다 한 번에 하나의 값을 반환합니다. 이터레이터가 모든 값을 반환하고 나면 소진(exhausted)되어 재사용할 수 없습니다.

이터레이터를 책의 책갈피처럼 생각해 보세요:

  • 시퀀스에서 현재 위치를 기억합니다
  • 다음 항목을 요청할 수 있습니다
  • 끝에 도달하면, 새 이터레이터를 만들지 않는 한 되돌아갈 수 없습니다

이터러블과 이터레이터의 핵심 차이는 다음과 같습니다:

  • 이터러블(iterable)반복할 수 있는 대상입니다(예: 리스트)
  • 이터레이터(iterator)는 실제로 반복을 수행하는 객체입니다(리스트를 따라가며 값을 꺼내는 메커니즘)
python
# 리스트는 이터러블입니다
numbers = [1, 2, 3]
 
# 이터러블에서 이터레이터 얻기
iterator = iter(numbers)
 
# 이터레이터는 별도의 객체입니다
print(type(numbers))    # Output: <class 'list'>
print(type(iterator))   # Output: <class 'list_iterator'>

35.2.2) for 반복문에서의 이터레이터

for 반복문을 작성하면 Python은 내부적으로 자동으로 이터레이터를 생성합니다:

python
numbers = [10, 20, 30]
 
# 여러분이 작성하는 코드:
for num in numbers:
    print(num)
 
# Python이 내부적으로(개념적으로) 하는 일:
# 1. iter(numbers)를 호출해 이터레이터를 얻습니다
# 2. 이터레이터에 next()를 반복 호출합니다
# 3. 이터레이터가 StopIteration을 발생시키면 멈춥니다

이를 명시적으로 작성하면 다음과 같습니다:

python
numbers = [10, 20, 30]
 
# 수동 반복(for가 자동으로 하는 일)
iterator = iter(numbers)
try:
    print(next(iterator))  # Output: 10
    print(next(iterator))  # Output: 20
    print(next(iterator))  # Output: 30
    print(next(iterator))  # Would raise StopIteration
except StopIteration:
    print("No more items")  # Output: No more items

for 반복문은 StopIteration 예외를 자동으로 처리하기 때문에, 일반적인 코드에서는 이를 볼 일이 없습니다.

Yes

No -> StopIteration

for item in iterable:

Python이 iter(iterable) 호출

iterator 객체를 얻음

Python이 next(iterator) 호출

더 많은 항목이 있나?

item에 할당

반복문 본문 실행

반복문 종료

35.2.3) 이터레이터로서의 파일 객체

파일 객체는 이터레이터의 훌륭한 예입니다. 파일을 반복하면 한 번에 한 줄씩 읽습니다:

python
# 샘플 파일 만들기
with open("students.txt", "w") as file:
    file.write("Alice\n")
    file.write("Bob\n")
    file.write("Charlie\n")
 
# 파일을 한 줄씩 읽기
with open("students.txt", "r") as file:
    for line in file:
        print(line.strip())  # Output: Alice, Bob, Charlie (on separate lines)

파일 객체는 이터러블이면서 동시에 이터레이터입니다. 파일 객체에 iter()를 호출하면 자기 자신을 반환합니다:

python
with open("students.txt", "r") as file:
    iterator = iter(file)
    print(file is iterator)  # Output: True (same object)
    
    # 줄을 수동으로 읽기
    print(next(iterator))  # Output: Alice
    print(next(iterator))  # Output: Bob
    print(next(iterator))  # Output: Charlie

이는 Python이 파일 전체를 메모리에 올리지 않고, 요청할 때마다 한 줄씩 읽기 때문에 메모리 효율적입니다.

35.2.4) 이터레이터로서의 range 객체

range 객체는 필요할 때 숫자를 생성하는 이터러블입니다:

python
# range는 이터러블입니다
numbers = range(1, 4)
print(type(numbers))  # Output: <class 'range'>
 
# range에서 이터레이터 얻기
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'range_iterator'>
 
# 이터레이터 사용하기
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3

range는 모든 숫자를 메모리에 저장하지 않고 요청될 때 각 숫자를 계산하기 때문에 메모리 효율적입니다:

python
# 이 range는 100만 개의 숫자를 나타내지만 메모리는 최소로 사용합니다
large_range = range(1000000)
print(type(large_range))  # Output: <class 'range'>
 
# 이터레이터 얻기
iterator = iter(large_range)
print(next(iterator))  # Output: 0
print(next(iterator))  # Output: 1
# ... 100만 개 값에 대해 계속할 수 있습니다

35.2.5) 딕셔너리 이터레이터

딕셔너리는 키, 값, 아이템에 대해 서로 다른 이터레이터를 제공합니다:

python
student = {"name": "Alice", "age": 20, "grade": "A"}
 
# 키를 대상으로 반복(기본 동작)
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)
 
# 키 이터레이터를 명시적으로 얻기
keys_iterator = iter(student.keys())
print(next(keys_iterator))  # Output: name
print(next(keys_iterator))  # Output: age
 
# 값을 대상으로 반복
values_iterator = iter(student.values())
print(next(values_iterator))  # Output: Alice
print(next(values_iterator))  # Output: 20
 
# 아이템(키-값 쌍)을 대상으로 반복
items_iterator = iter(student.items())
print(next(items_iterator))  # Output: ('name', 'Alice')
print(next(items_iterator))  # Output: ('age', 20)

35.2.6) 이터레이터는 소진됩니다

이터레이터의 중요한 성질은 한 번만 사용할 수 있다는 것입니다. 한 번 소진되면 초기화되지 않습니다:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
# 이터레이터를 처음 한 번 통과하기
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3
 
# 이제 이터레이터는 소진되었습니다
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("Iterator exhausted")  # Output: Iterator exhausted
 
# 다시 반복하려면 새 이터레이터를 만듭니다
iterator = iter(numbers)
print(next(iterator))  # Output: 1 (fresh start)

이는 이터러블 자체와는 다릅니다. 이터러블은 여러 번 반복할 수 있습니다:

python
numbers = [1, 2, 3]
 
# 첫 번째 반복
for num in numbers:
    print(num)  # Output: 1, 2, 3
 
# 두 번째 반복(문제없음 - 새 이터레이터를 만듭니다)
for num in numbers:
    print(num)  # Output: 1, 2, 3

35.3) iter()next()로 이터러블을 한 단계씩 진행하기

35.3.1) iter() 함수

iter() 함수는 이터러블을 받아 이터레이터를 반환합니다. 이것이 반복 프로토콜의 첫 단계입니다:

python
# 서로 다른 이터러블에서 이터레이터 만들기
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator))  # Output: <class 'str_iterator'>
 
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator))  # Output: <class 'set_iterator'>

각 이터러블 타입은 자신만의 특화된 이터레이터 타입을 반환하지만, 동작 방식은 모두 같습니다—next()를 호출해 다음 값을 얻습니다.

35.3.2) next() 함수

next() 함수는 이터레이터에서 다음 항목을 가져옵니다. 더 이상 항목이 없으면 StopIteration을 발생시킵니다:

python
colors = ["red", "green", "blue"]
iterator = iter(colors)
 
# 한 번에 하나씩 항목 가져오기
print(next(iterator))  # Output: red
print(next(iterator))  # Output: green
print(next(iterator))  # Output: blue
 
# 더 이상 항목이 없음
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("No more colors")  # Output: No more colors

35.3.3) next()에 기본값 제공하기

next()에 두 번째 인수로 기본값을 제공할 수 있습니다. Iterator가 소진되면 StopIteration 예외를 발생시키는 대신, next()는 지정한 기본값을 반환합니다:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
print(next(iterator))           # Output: 1
print(next(iterator))           # Output: 2
print(next(iterator))           # Output: 3
print(next(iterator, "Done"))   # Output: Done (default value, no exception)
print(next(iterator, "Done"))   # Output: Done (still exhausted)

이는 예외 처리를 하지 않고도 반복의 끝을 부드럽게 처리하고 싶을 때 유용합니다:

35.4) __iter____next__로 커스텀 이터레이터 만들기

35.4.1) 커스텀 이터레이터를 만드는 이유

Python의 내장 이터러블(리스트, 문자열, 파일)은 대부분의 일반적인 경우를 커버합니다. 하지만 때로는 특수한 동작을 위해 자신만의 이터러블 객체를 만들어야 합니다:

  • 커스텀 로직으로 시퀀스 생성하기
  • 직접 설계한 데이터 구조를 반복하기
  • 대규모 데이터셋을 메모리 효율적으로 반복하기
  • 지연 평가(lazy evaluation) 구현하기(필요할 때만 값 계산)

커스텀 이터레이터를 만들려면 __iter__()__next__()라는 두 개의 특별한 메서드를 구현해야 합니다.

35.4.2) 이터레이터 프로토콜

객체를 이터레이터로 만들려면 다음을 구현해야 합니다:

  1. __iter__(): 이터레이터 객체 자기 자신을 반환합니다(보통 self)
  2. __next__(): 시퀀스의 다음 값을 반환하거나, 끝나면 StopIteration을 발생시킵니다
python
class SimpleCounter:
    """start부터 end까지 세는 이터레이터입니다."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        """이터레이터 객체(self)를 반환합니다."""
        return self
    
    def __next__(self):
        """다음 값을 반환하거나 StopIteration을 발생시킵니다."""
        if self.current > self.end:
            raise StopIteration
        
        value = self.current
        self.current += 1
        return value
 
# 커스텀 이터레이터 사용하기
counter = SimpleCounter(1, 5)
 
for num in counter:
    print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5

어떤 일이 일어나는지 분해해 봅시다:

  1. for 반복문은 iter(counter)를 호출하고, 이는 counter.__iter__()를 호출해 counter 자체를 돌려받습니다
  2. 반복문은 next(counter)를 반복 호출하며, 이는 counter.__next__()를 호출합니다
  3. __next__()가 호출될 때마다 다음 숫자를 반환하고 current를 증가시킵니다
  4. current > end가 되면 __next__()StopIteration을 발생시키고 반복문이 멈춥니다

35.4.3) 커스텀 이터레이터의 수동 사용

커스텀 이터레이터도 iter()next()로 수동으로 사용할 수 있습니다:

python
counter = SimpleCounter(10, 13)
 
# 이터레이터 얻기(자기 자신을 반환)
iterator = iter(counter)
print(iterator is counter)  # Output: True
 
# 값을 수동으로 얻기
print(next(iterator))  # Output: 10
print(next(iterator))  # Output: 11
print(next(iterator))  # Output: 12
print(next(iterator))  # Output: 13
 
# 이제 소진됨
try:
    print(next(iterator))
except StopIteration:
    print("Counter exhausted")  # Output: Counter exhausted

35.4.4) 이터레이터는 소진됩니다(다시 보기)

이터레이터는 한 번만 사용할 수 있다는 점을 기억하세요:

python
counter = SimpleCounter(1, 3)
 
# 첫 번째 반복
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# 두 번째 반복(동작하지 않음 - 이터레이터가 소진됨)
for num in counter:
    print(num)  # Nothing printed - iterator is already exhausted

다시 반복하려면 새 인스턴스를 만들어야 합니다:

python
# 반복마다 새 counter 만들기
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3
 
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3 (new iterator)

35.4.5) (이터레이터가 아니라) 이터러블 클래스 만들기

종종 여러분은 이터러블이면서, 매번 새 이터레이터를 만들어 주는 클래스를 원합니다. 이를 위해 이터러블과 이터레이터를 분리합니다:

python
class CounterIterable:
    """매번 새 카운터 이터레이터를 만드는 이터러블입니다."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        """매번 새 이터레이터를 반환합니다."""
        return CounterIterator(self.start, self.end)
 
class CounterIterator:
    """실제로 카운팅을 수행하는 이터레이터입니다."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value
 
# 이제 여러 번 반복할 수 있습니다
counter = CounterIterable(1, 3)
 
# 첫 번째 반복
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# 두 번째 반복(__iter__가 새 이터레이터를 만들기 때문에 동작)
for num in counter:
    print(num)  # Output: 1, 2, 3

이 패턴은 관심사를 분리합니다:

  • CounterIterable는 이터러블로서—이터레이터를 만드는 방법을 압니다
  • CounterIterator는 이터레이터로서—값을 어떻게 진행하며 꺼낼지 압니다

35.4.6) 실용 예제: 커스텀 데이터 구조 반복하기

커스텀 데이터 구조를 위한 이터레이터를 만들어 봅시다—간단한 재생목록(playlist)입니다:

python
class Playlist:
    """반복 가능한 음악 재생목록입니다."""
    
    def __init__(self):
        self.songs = []
    
    def add_song(self, title, artist):
        """재생목록에 곡을 추가합니다."""
        self.songs.append({"title": title, "artist": artist})
    
    def __iter__(self):
        """재생목록을 위한 이터레이터를 반환합니다."""
        return PlaylistIterator(self.songs)
 
class PlaylistIterator:
    """재생목록의 곡들을 하나씩 진행하기 위한 이터레이터입니다."""
    
    def __init__(self, songs):
        self.songs = songs
        self.index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= len(self.songs):
            raise StopIteration
        
        song = self.songs[self.index]
        self.index += 1
        return song
 
# 재생목록 사용하기
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
 
# 곡을 반복하기
print("Now playing:")
for song in playlist:
    print(f"  {song['title']} by {song['artist']}")
# Output: Now playing:
# Output:   Imagine by John Lennon
# Output:   Bohemian Rhapsody by Queen
# Output:   Hotel California by Eagles
 
# 다시 반복 가능(새 이터레이터를 만듭니다)
print("\nReplay:")
for song in playlist:
    print(f"  {song['title']}")
# Output: Replay:
# Output:   Imagine
# Output:   Bohemian Rhapsody
# Output:   Hotel California

35.4.7) 커스텀 이터레이터를 사용해야 할 때

다음과 같은 경우에 커스텀 이터레이터를 만드세요:

  1. 지연 평가가 필요할 때: 모든 값을 저장하지 않고 필요할 때 생성합니다
  2. 커스텀 데이터 구조가 있을 때: for 반복문과 함께 동작하도록 이터러블로 만듭니다
  3. 특별한 반복 로직이 필요할 때: 항목 건너뛰기, 값 변환, 복잡한 진행 규칙 구현 등
  4. 메모리 효율이 중요할 때: 큰 시퀀스를 저장하지 않고 생성합니다

하지만, 36장에서는 yield 키워드를 사용해 훨씬 간단하게 이터레이터를 만들 수 있는 제너레이터(generator)를 배우게 됩니다. 제너레이터는 보통 __iter__()__next__()를 직접 구현하는 것보다 더 간결하고 이해하기 쉬워서 선호됩니다.

커스텀 이터레이터를 만드는 방법을 이해하면, 비록 종종 제너레이터를 대신 사용하게 되더라도 Python의 반복 프로토콜이 어떻게 동작하는지에 대한 통찰을 얻을 수 있습니다. 여기서 배운 개념들—__iter__(), __next__(), StopIteration—은 다음 장에서 다룰 제너레이터와 다른 고급 반복 기법을 이해하는 데 기본이 됩니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai