티스토리 뷰

반응형

오늘 다뤄보려는 내용은 파이썬의 반복자(Iterator)이다. 많은 사람들이 파이썬의 "반복자"에 대해서 궁금해하는데, 정작 이 반복자는 파이썬의 반복문 뒤에 숨어서 기능하는 객체이며, 중요한 점은 반복자 자체에 대한 이해보다는 "반복가능한(iterable)" 기능을 구성하는 프로토콜에 대해서 이해하는 것이다.

iterable 프로토콜은 파이썬의 for 구문의 핵심이며, 이 구조를 이해하면 반복가능한 커스텀 데이터 타입을 정의하여 만드는 것이 얼마든지 가능하다.

연속열

파이썬의 많은 타입들이 비슷한 동작을 공유하는 경우가 있다. 리스트나 튜플, 문자열과 range 객체는 단일 값이 아니라, 그 내부에 여러 개의 요소들을 포함하는 컨테이너이면서, 각 요소들간의 순서가 존재한다. (set, dict의 경우에는 집합 컨테이너 타입이기는 하지만, 그 내부의 요소들은 특별한 순서를 가지지는 않는다.)

연속열 자체가 어떤 기능을 가져야 한다는 것은 별도의 확립된 명세가 존재하는 것은 아니지만, 보통 다음과 같은 기능을 공통적으로 제공한다. 이는 결국 "여러 요소들이 정해진 순서를 가지고 내부에 나열되어 있다"는 특성에서 유도될 수 있는 성질들이다. (그리고 리스트와 관련된 내용에서 100% 다루게 되는 기능들이기도 하다.)

  • x in s : 연속열 s안에 요소 x가 있는지 검사한다.
  • s + t : 두 개의 연속열을 하나로 합친다.
  • s * n : 연속열을 n회 반복한 새로운 연속열을 만든다.
  • s[i] : 연속열 내의 i 번째 요소를 구한다.
  • len(s) : 연속열의 길이를 구한다.
  • min(s), max(s) : 연속열 내에서 최소, 최대 값을 구한다.
  • s.index(x) : 연속열 내에 x 요소의 위치를 구한다.
  • s.count(x) : 연속열 내의 x의 개수를 구한다.

반복자

파이썬에는 연속열이 아닌 집합 컨테이너들도 있다. 예를 들면 사전의 키집합이나, 집합 타입(set)같은 것들이 그러하다. 이들은 내부에 순서가 없기 때문에 s[i]와 같은 인덱스 기반 연산은 수행할 수 없다. 하지만 이런 컨테이너들이 연속열과 똑같이 쓰일 수 있는 부분이 있는데, 그것은 바로 for 문이다. 우리는 for 문에서 리스트의 각 원소를 순회하는 것과 마찬가지로, 사전의 각 키들을 순회하거나, 문자열의 각 낱개 글자들, 그리고 집합의 각 원소들을 순회할 수 있다.

이 반복 개념을 지원하게 해주는 것을 이터레이터(반복자) 프로토콜이라한다. 물론 파이썬에서 명시적으로 어떤 프로토콜을 선언하는 것은 불가능하다. 다만, 일종의 비정규 프로토콜인 반복자의 개념을 이해하고 있으면 우리가 커스텀 클래스를 만들었을 때, 해당 클래스가 for 문에 적용하여 반복을 지원할 수 있게 할 수 있다.

주의할 것은 리스트나 세트 그 자체가 항상 반복자인 것은 아니다. (사실 엄밀히 리스트는 그 자체가 반복자인 것은 맞다), 이들 타입은 단지 반복자 프로토콜을 따르고 있고, 이 프로토콜에 의해서 "반복자" 객체를 생성할 수 있고, 이 반복자 객체에 의해 반복문을 돌게 되는 것이다.

백스테이지 : for 문은 어떻게 동작하는가?

어떤 정수의 리스트가 있고, 이 리스트를 for 문으로 순회하는 다음의 예시를 보자.


a = [1, 2, 3, 4, 5]
for i in a:
    print(i)

여기서 for 구문은 연속열 a의 각 원소에 대해 순차적으로 i라는 이름을 붙여서 for 블록 내의 코드를 반복 적용한다.

실제로 파이썬의 for 구문은 다음과 같이 동작한다.

  1. 순회하려는 컨테이너로부터 반복자 객체를 얻는다.
  2. 반복자 객체를 next() 함수에 던져넣어 리턴되는 값에 대해서 블럭을 수행한다.
  3. StopIteration 예외가 던져질 때까지 2를 반복한다.

반복자 객체를 얻는 방법은 컨테이너 객체를 iter() 함수에 전달하는 것이다. 이 함수는 container.__iter__()를 호출하여, 이터레이터 프로토콜을 따르는 컨테이너로 하여금 반복자 객체를 요구한다.

반복자 객체는 타입이 정해진 것은 아니지만, 두 가지의 메소드를 구현하고 있을 것이 요구된다.

  1. __iter__() : 반복자 자신을 리턴하면 된다.
  2. __next__() : 다음 반복 요소를 리턴하고, 더 이상 리턴할 요소가 없으면 StopIteration 예외를 일으킨다. 이 메소드는 next() 내장함수에 의해서 호출된다.

실제로 리스트에 대한 for문 순회는 다음과 똑같이 동작한다.


In [20]: a = [1,2,3,4,5]

# 리스트 a로부터 반복자를 획득한다. 
In [21]: i = iter(a)  

# 반복자의 정체는 리스트 반복자 객체이며,
In [22]: i
Out[22]: <list_iterator at 0x52bf8d0>

# 이 반복자는 `__iter__`, `__next__` 요소를 가지고 있다. 
In [23]: dir(i)
Out[23]:
[...
 '__iter__',
...
 '__next__',
...
]

# 이 반복자를 이용해서 리스트의 각 원소는 다음과 같이 순회한다. 
In [24]: while True:
    ...:     try:
    ...:         print(next(i))
    ...:     except StopIteration:
    ...:         break
    ...:
1
2
3
4
5

반복자의 동작원리는 결국 __next__() 메소드를 정의해서 순차적으로 내놓고자 하는 값들을 내놓은 후 마지막에 StopIteration예외를 일으키는 구조만 유지하면 사실 어떤 것이든 될 수가 있고, 따라서 커스텀 클래스를 만들 때, 해당 클래스의 인스턴스 객체가 for ... in문에 사용될 수 있게 하고 싶다면, 반복자를 생성해주면 된다.

터무니없는 예제

그래서 그게 가능하다는 것을 보여주기 위한 예를 하나 만들어보도록 하겠다. 학생의 이름과 영어, 수학, 물리, 화학 점수를 저장할 수 있는 클래스를 하나 만들어보자. 이 클래스는 for ... in 문에 사용되어 각 과목의 점수를 순회하도록 한다.


class Student:
    def __init__(self, name, math, eng, phy, chem):
        self.name = name
        self.math = math
        self.eng = eng
        self.phy = phy
        self.chem = chem
        
    # 반복자는 자기 자신을 리턴한다. 
    # 별도의 클래스를 따로 정의해서 만들어도 된다만...
    def __iter__(self):
        self.i = 4
        return self
    
    # `next()` 함수에 대응하기 위해 `__next__` 메소드도 만든다. 
    def __next__(self):
        x = (self.chem, self.phy, self.eng, self.math)
        self.i -= 1
        if self.i < 0:
            raise StopIteration
        return x[self.i]

굉장히 이상하게 만들어지긴 했지만, 최소한 iterable 프로토콜이 요구하는 조건은 만족한다.

  1. __iter__ 메소드를 가지고 있으며, 자기 자신을 리턴한다.
  2. 자기 자신이 반복자이므로, __next__ 메소드를 구현하고 있으며, 각 과목의 점수를 순서대로 리턴한 다음, 마지막에 StopIteration 예외를 던진다.

그럼 for...in 구문으로 테스트해보자.


a = Student('test', 80, 90, 70, 60)
for i in a:
    print(i)
# 80
# 90
# 70
# 60

여기서 매우 흥미로운 지점을 짚고 넘어갈 수 있다. 내장함수 중에 sum이라는 함수를 알거나 최소한 들어본 적 있겠지? 이 함수의 도움말을 보면....


In [60]: sum?
Signature: sum(iterable, start=0, /)
               ^^^^^^^^
Docstring:
Return the sum of a 'start' value (default: 0) plus an iterable of numbers

내장함수 sum()은 기본적으로 반복가능한 객체를 받는다고 한다. 그런데 앞에서 만든 Student는 반복가능하도록 디자인 되었다. (즉 iterable 프로토콜을 따르고 있다.) 따라서 해당 학생의 전과목 총점을 계산하는 것은 sum() 함수로 계산가능하다.


sum(a)
# 300 (80 + 90 + 70 + 60)

이상으로 파이썬의 반복자에 대해서 살펴보았다. 결론만 요약하자면,

  1. 어떤 파이썬 타입 혹은 클래스가 iterable하다는 것은 for...in문을 통해 반복될 수 있다는 것을 의미한다.
  2. iterable한 타입은 __iter__ 메소드를 가지고 있고, 이 메소드는 반복자(iterator)를 리턴한다.
  3. 반복자는 그 스스로가 iterable하면서 __next__ 메소드를 가지고서 어떤 컨테이너의 개별 요소를 반복하게 해준다.
  4. 이 프로토콜을 이해하고, 필요한 메소드들을 구현할 수 있다면, 커스텀 클래스도 for...in문에 쓸 수 있게끔 만들 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함