반응형

Python에서 iteration(반복)은 iterator 프로토콜을 기반으로 동작한다.

이 원리를 이해하려면 아래 개념들을 알아야 한다.

 

1. Iterable (Iterate 가능한 객체)

Iterable은 __iter__() 메서드를 구현한 객체이다. for 문 등 반복문에서 iterate할 수 있는 객체를 말한다.

Iterable 객체는 내부적으로 __iter__() 메서드를 가지고 있어야 하며, 이 메서드를 호출하면 iterator를 반환한다.

다음과 같은 종류의 iterable이 있다:

  • sequence형 자료구조: list, tuple, dict, str, range, bytes, bytearray 등.
  • 반복가능한 다른 자료구조: dict, set, frozen_set, zip, map, filter 등.
  • iterator와 generator: iterator는 아래에서 다루고, generator는 별도의 글(https://comgu.tistory.com/entry/Python-제너레이터Generator)에서 다룬다.

특징:

  • iter() 함수를 호출하면 iterator 객체를 반환한다. iter()를 호출하는 것은 내부 구현된 __iter__() 메서드를 호출하는 것과 동일하다.
my_list = [1, 2, 3]
iterator = iter(my_list)  # iterator 반환
print(iterator)
<list_iterator object at 0x00000220BEB89C00>

위 코드에서 볼 수 있듯이, list 객체에 대한 iter() 호출은 list 전용 iterator인 list_iterator 객체를 반환한다.

print(iter("abc"))
<str_ascii_iterator object at 0x0000023A9F72A6B0>

str 객체에 대한 iterator는 str_ascii_iterator 타입 객체임을 알 수 있다.

 

2. Iterator

Iterator는 iterable 객체에서 값을 하나씩 꺼내오는 객체이다. Iterator는 반드시 __iter__()__next__() 메서드를 가지고 있다.

  • __iter__(): iterator 자신을 반환한다(iterator는 자기 자신이 iterable이기도 하다).  iter()를 호출하는 것은 내부 구현된 __iter__() 메서드를 호출하는 것과 동일하다.
  • __next__(): iterable의 다음 값을 반환한다. 더 이상 반환할 값이 없으면 StopIteration 예외를 발생시킨다. next()를 호출하는 것은 내부 구현된 __next__() 메서드를 호출하는 것과 동일하다.
my_list = [1, 2, 3]
iterator = iter(my_list)

print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3
print(next(iterator))  # 더 이상 값이 없으면 StopIteration 발생
1
2
3
Traceback (most recent call last):
  File "C:\work\python_practice\iter.py", line 8, in <module>
    print(next(iterator))
          ^^^^^^^^^^^^^^
StopIteration

 

3. Iterator Protocol

Iterator와 Iterable이 상호작용하는 규칙을 말한다. 이 규칙에 따라 파이썬의 for문과 같은 iteration 구조가 작동한다.

Iterator Protocol의 핵심:

  1. 어떤 객체가 __iter__() 메서드를 구현하면 iterable 객체가 된다. 즉 반복 가능해지게 된다.
  2. Iterator는 __iter__()__next__() 메서드를 모두 구현한다.
    • iterator의 __iter__()는 자기 자신을 반환해야 한다.

 

4. Iteration의 동작 원리 (for문 기준)

동작 단계:

  1. iter() 호출
    • for 문은 내부적으로 대상 iterable 객체에 iter()를 호출해 iterator 객체를 확보한다.
  2. next() 호출
    • 반복이 시작되면 iterator 객체에 next()를 호출해 순서대로 iterable의 값을 가져온다.
  3. 종료 조건
    • StopIteration 예외가 발생하면 반복을 종료한다.

아래 코드와 같은 방식으로 동작한다고 보면 된다:

my_list = [1, 2, 3]

iterator = iter(my_list)  # 1. iter() 호출

while True:
    try:
        item = next(iterator)  # 2. next() 호출
        print(item)
    except StopIteration:  # 3. StopIteration 예외 발생 시 반복 종료
        break

 

5. 커스텀 Iterable과 Iterator

임의의 클래스에서 iterator protocol을 구현하면, 직접 iterable 객체를 만들 수 있다.

iterable에는 __iter__() 메소드를 구현하고, iterator에는 __iter__()__next__() 메소드를 구현하면 된다.

class SimpleIterable:
    """0부터 지정된 개수만큼 숫자를 생성하는 iterable 클래스"""
    def __init__(self, count):
        self.count = count

    def __iter__(self):
        return SimpleIterator(self.count)


class SimpleIterator:
    """숫자를 하나씩 생성하는 iterator 클래스"""
    def __init__(self, count):
        self.current = 0
        self.count = count

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.count:
            raise StopIteration
        value = self.current
        self.current += 1
        return value


simple_iterable = SimpleIterable(5)

for number in simple_iterable:
    print(number)
0
1
2
3
4

 

Iterator 자체가 iterable로 동작할 수 있다. Iterator가 __iter__() 메서드를 구현하기 때문이다(자기 자신을 반환하는 함수).

따라서 iterator는 항상 iterable이기도 하다. 이 설계는 파이썬에서 흔히 사용하는 패턴으로, 복잡성을 줄이고 간결하게 반복 동작을 처리할 수 있도록 도와준다.

이를 통해 for 루프와 같은 반복 구조에서 iterator를 iterable처럼 직접 사용할 수도 있다.

class SimpleIterator:
    """숫자를 하나씩 생성하는 iterator 클래스"""
    def __init__(self, count):
        self.current = 0
        self.count = count

    def __iter__(self):
        return self  # 자기 자신을 반환

    def __next__(self):
        if self.current >= self.count:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

# SimpleIterator를 직접 사용
iterator = SimpleIterator(3)

print(iter(iterator))  # 자기 자신을 반환
for number in iterator:
    print(number)

# 한 번 더 반복하면 아무것도 출력되지 않음 (소진된 iterator)
for number in iterator:
    print(number)
<__main__.SimpleIterator object at 0x000001F46BC9A450>
0
1
2

 

※ Iterator가 iterable의 역할을 동시에 수행할 수 있는데, 굳이 둘이 나눠진 이유는?

역할의 분리(Seperation of Concerns)

  • Iterable은 반복 가능한 데이터 구조를 정의한다. List, tuple, dict 등은 모두 __iter__() 메서드를 제공하여 반복을 시작할 준비가 되있는 객체들이다.
  • 반면에, iterator는 반복의 상태를 추적해서, 실제로 데이터를 제공하는 역할을 수행한다. __next__()를 통해 데이터를 하나씩 반환받는다.

이렇게 역할을 분리하면 2가지 장점이 있다:

  • 재사용성: iterable 객체는 iterate 요청이 들어올 때마다 새로운 iterator를 생성할 수 있다. 그래서 같은 iterable을 여러 for 루프에서 독립적으로 사용할 수 있다.
  • 상태 관리: iterator는 iterate 상태(현재 위치 등)을 내부적으로 유지한다. 반면 iterable은 데이터를 정의 및 제공만 하면 된다.

덕분에 동일한 iterable에 대해 여러 개의 iterator를 생성할 수 있다.

nums = [1, 2, 3]
iter1 = iter(nums) # 독립된 iterator
iter2 = iter(nums) # 독립된 iterator

print(next(iter1))  
print(next(iter2))
1
1

또한 iterable 객체를 여러 번 재사용 할 수 있다. (각 for문에서는 iterable로부터 매번 새로 iterator를 제공받는다.)

nums = [1, 2, 3]
for num in nums:
    print(num)

for num in nums:  # 반복 가능
    print(num)
1
2
3
1
2
3

iterator는 1회용이다. 여러 번 사용할 수 없으므로 1회 사용 이후 "소진" 되어 버린다.

따라서, 같은 데이터를 여러번 반복하기 위해서는 iterable에 대해 iter()를 호출해 새로운 iterator를 반환받는 과정이 필요하다 (4. Iteration의 동작 원리에 작성했듯이 for 문은 iterator 확보 동작을 내부에서 자동으로 처리한다)

nums = iter([1, 2, 3])
for num in nums:
    print(num)

for num in nums:  # 두 번째 반복 불가능
    print(num)
1
2
3

위와 같은 이유로 iterator와 iterator는 분리되었다. 하지만 1회성으로 반복하기 위한 간단한 케이스에서는 둘을 하나로 합쳐서 사용해도 상관 없다. 

 

6. 장점

  • 메모리 효율: 데이터를 한 번에 모두 메모리에 올리지 않고 필요한 시점에 값을 생성해 처리할 수 있다.
    • 이 내용은 list나 str 같이 이미 모든 데이터가 메모리 상에 존재하는 데이터구조에 대해서는 해당되지 않는 장점이다. list의 iterator는 단순히 메모리에 있는 데이터를 순차적으로 iterate하기 떄문에 메모리 효율이 발생하지 않는다.
    • 반면에 iterator와 generator를 잘 활용하면 데이터를 한 번에 모두 메모리에 올리지 않고, 필요한 값을 하나씩 생성해서 반환할 수 있다. "5. 커스텀 Iterable과 Iterator"의 SimpleIterable/SimpleIterator와 일반적으로 사용되는 range가 iterator가 이렇게 사용된 케이스에 해당된다. generator는 다른 원리로 동작하지만, 메모리를 효율적으로 사용한다는 점에서는 동일하다(generator에 대해서는 https://comgu.tistory.com/entry/Python-제너레이터Generator 참고).
  • 유연성: 모든 반복 가능한 객체를 통일된 방식으로 다룰 수 있다.
    • iter()와 next()라는 통일된 메서드를 사용해서 반복을 수행하는 메커니즘이 정립되어 있기 때문이다.

 

요약

  • 파이썬의 iterator protocol은 iterable과 iterator가 상호작용하는 규칙을 말한다. 이 규칙에 따라 파이썬의 for문과 같은 iteration 구조가 작동한다.
  • iterable은 list, str, range 등과 같이 iterate할 수 있는 객체를 말한다. iterable을 반환하는 __iter__() 메소드가 구현되있어야 한다.
  • iterable은 iterable 객체에서 값을 하나씩 꺼내오는 객체이다. 반드시 __iter__() __next__() 메서드를 가지고 있다.
  • for 문과 같은 반복문에서는 iterable와 iterator에 대해 iter()next()를 호출하여서, iteration을 처리할 수 있다.
  • iterator를 잘 활용하면 메모리를 효율적으로 사용할 수 있다.
  • Iterator Protocol은 iter()next()라는 표준 메서드를 제공하기 떄문에, 반복 가능한 객체를 일관적인 방식으로 다룰 수 있는 표준적인 API를 제공하는 것이다.
반응형

+ Recent posts