http://pythonstudy.xyz/python/article/23-Iterator%EC%99%80-Generator

for 문과 반복자

파이썬에서는 “반복자”라는 특별한 성질을 가진 타입들이 있다. 반복자는 영어로 iterator라고 하는데, 파이썬에서 반복자는 기술적으로는 단순히 __next__() 라는 메소드를 가지고 있는 객체를 말한다. (사실 반복자 그 자체는 제너레이터의 일종으로 볼 수도 있고, 기술적으로 제너레이터이다라고 해도 틀린표현은 아니다.) 보통 반복자들은 일련의 연속적인 값 혹은 특정한 규칙을 가진 수열을 계산하는 값을 내부에 가지고 있고, 이를 __next__() 가 호출될 때마다 내부적으로 관리하는 수열의 다음항을 리턴할 수 있는 객체가 된다.

참고로 내장함수의 도움말을 살펴보면 다음과 같다. 여기서는 iterator 라는 표현을 직접적으로 쓰고 있으며, 반복자로부터 다음번 아이템 얻어 리턴한다고 쓰여있다.

In [1]: next? Docstring: next(iterator[, default])  
Return the next item from the iterator. 
If default is given and the iterator is exhausted, 
it is returned instead of raising StopIteration. 
Type: builtin_function_or_method

참고로 반복자가 더 이상 만들어 낼 다음번 항이 없는 경우에는 StopIteration 예외를 일으키고, 이는 곧 순회(iteration)의 끝을 의미한다.

반복가능 (iterable) 프로토콜

파이썬에서 반복가능하다는 말은 결국 for ... in 문에 적용가능하다는 말과 동치이고, 기술적으로는 이터레이터(iterator, 반복자)를 가지고 있는 객체라는 의미이다. 반복가능한 타입의 객체로부터 반복자를 얻어내는 내장함수는 iter() 함수이다. (뒤에서 살펴보겠지만, next(x)x.__next__()를 호출하듯이, iter(x)x.__iter__()를 호출한다. 사실 이것이 반복가능 프로토콜의 핵심이다. )역시 반복 가능한 객체에 대한 힌트를 얻기 위해서 이 함수의 도움말을 살펴보도록 하자.

In [4]: iter? Docstring: iter(iterable) 
-> iterator iter(callable, sentinel) 
-> iterator  Get an iterator from an object. 
In the first form, the argument must supply its own iterator, 
or be a sequence. In the second form, the callable is called until it returns the sentinel. 
Type: builtin_function_or_method

우리는 여기서 많은 것을 볼 수 있다.

  1. iter 함수는 어떤 객체로부터 반복자를 얻어서 리턴한다. (list를 만든 후 dir(list)를 통해보면, 디셔너리 자료형이 반환되는데, 그안에 __iter__함수가 존재하는 것을 볼 수 있다. iter()함수는 바로 이 __iter__()함수의 리턴값을 반환한다)
  2. 반복가능한 객체는 반복자를 이미 가지고 있거나, 아니면 그 스스로가 연속열이다.
  3. 반복자 뿐만 아니라 특정한 종결값을 리턴할 때까지 계속 어떠한 값을 리턴할 수 있는 함수도 반복가능으로 취급한다.

지금까지의 힌트를 취합하면 다음과 같은 사실들을 알아내었다고 정리할 수 있다.

  1. iter() 함수를 이용하면 iterable한 객체의 반복자를 얻을 수 있다.
  2. next() 함수를 이용하면 반복자의 매 항을 얻을 수 있다.
  3. next() 함수를 이용했을 때 반복자가 더 이상 내 줄 값이 없으면 StopIteration 예외를 일으킨다.
  4. 그리고 range() 함수가 리턴하는 객체는 for ... in 문에 사용할 수 있으니, iterable 하다.

for … in 루프의 구조

그러면 이러한 사실로부터 우리는 파이썬의 for ... in 문을 while 문으로 재구성해볼 수 있다.

for i in range(3):   print(3)

위 반복문은 우리가 알아낸 사실에 근거하여 다음과 같이 쓸 수 있다.

## range(3)은 iterable 한 객체를 리턴하고 
## iter() 함수를 이용하면 그로부터 반복자를 얻을 수 있다.  
x = range(3)

## range는 다음과 같은 네임스페이스를 갖는다. range()함수는 __iter__ 함수를 가지고 있는 것을 확인할 수 있다. 
dir(x)
>>> ['__bool__', '__class__', '__contains__', '__delattr__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', 
'__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', 'count', 'index', 'start', 'step', 'stop']
def customFor(x):
iteratorX = x.__iter__()   

## 이터레이터 객체를 갖는다면 이터레이터화 한다.

while True:      
	try:        
		i = next(iteratorX) 
		## 반복자로부터 다음 항을 얻는다.         
		return i      
	except StopIteration: 
		## 반복자가 고갈되면 StopIteration이 뜬다.        
		print("iteratrion finished.")        
		break  

# 0 # 1 # 2 # iteration finished.

그리고 파이썬의 for ... in 문은 실제로 이렇게 돌아간다. 임의의 리스트나 문자열, 튜플 등에 대해서 아래와 같이 반복자를 직접 얻어서 next() 함수를 계속 호출해볼 수 있다.

In [8]: x = iter([1,2,3,4])    
In [9]: x  Out[9]: <list_iterator at 0x222958f2b38>    
In [10]: next(x)  Out[10]: 1    
In [11]: next(x)  Out[11]: 2    
In [12]: next(x)  Out[12]: 3    
In [13]: next(x)  Out[13]: 4    
In [14]: next(x)  ---------------------------------------------------------------------------  
StopIteration Traceback (most recent call last)  <ipython-input-14-5e4e57af3a97> in <module>()  
----> 1 next(x)    StopIteration:

반복자

그렇다면 이번에는 반복자에 대해서 좀 더 알아보도록 하자. 반복자는 앞서 말했듯이 __next__() 메소드를 가지고 있는 객체라고 했다. 내장함수 next()는 모종의 약속에 의해서 인자로 받는 객체의 __next__() 메소드를 호출하고 그 결과를 리턴해주는 역할을 할 뿐이다.

커스텀 반복자 클래스

그렇다면 예를 들어서 피보나치 수열의 항을 순차적으로 리턴할 수 있는 반복자를 만들 수 있지 않을까?

class FibonacciSeq:   
    def __init__(self):     
    	self.a, self.b = 0, 1    

    def __next__(self):     
        self.a, self.b = self.b, self.a + self.b     
        ## 무한 수열이 될 수 있으니, 200보다 큰 값이 만들어지면 끝낸다.      
            if self.a > 200:       
                raise StopIteration     
                return self.a  
        ## 테스트 
        f = FibonacciSeq() 
        next(f)

# 1 next(f) # 1 next(f) # 3 next(f) # 5 next(f) # 8

대략 성공적이다. 하지만 이렇게 반복자를 만들더라도 iterable한 객체는 따로 존재한다. 즉 지금까지는 itrable한 객체가 있고, 여기에 iter() 함수를 통해서 반복자를 만들어서 각 항을 순회했다는 것이다. 즉 for ... in 문에서 필요한 것은 반복자 그 자체가 아닌 반복자를 생성할 수 있는 객체, 즉 iterable 한 객체이다.

하지만 방금 작성한 FibonacciSeq 클래스는 그 자체가 반복자이면서 반복가능한 객체가 될 수 있다. 왜냐하면 next()함수와 마찬가지로 iter() 함수는 인자로 받은 객체에 대해서 __iter__() 메소드를 호출하여 반복자를 얻기 때문이다. 이 역시 모종의 약속이 미리 정해져 있는 셈이다. 어쨌든 그 스스로가 반복자의 모든 요건을 갖추고 있으므로 __iter__()는 그 자신을 리턴하는 것으로만 간단히 정의하면 된다.

따라서 다음과 같이 피보나치 생성 클래스를 수정해보자. 수정하는 김에 한계값 자체는 생성시에 인자로 받을 수 있게끔 함께 변경한다.

class FibonacciSeq:   
    def __init__(self, upto=200):     
        self.limit = upto     
        self.a, self.b = 0, 1    

    def __iter__(self):     
        return self    

    def __next__(self):     
        self.a, self.b = self.b, self.a + self.b 
        
        if self.a > self.limit:       
            raise StopIteration     
            return self.a  

## 테스트 : 이제 for ... in 문에서도 잘 작동한다.
for f in FibonacciSeq(200):   
	print(f)

반복가능한 객체

내부에 __iter__(), __next__() 메소드를 가지는 객체를 만들기만 하면 이 객체는 기술적으로는 반복가능한 객체가 된다고 했다. 이를 통해서 특정한 하나의 값을 반복하는 리피터라든지, 여러 개의 연속열을 한꺼번에 순회할 수 있는 체인시퀀스 같은 도구를 만들 수도 있을 것이며, 그외에 내부 속성들을 for … in 문을 통해서 순회할 수 있는 객체도 디자인할 수 있을 것이다. 예를 들면 어떤 학생들의 시험 성적을 관리하는 코드에서 Student라는 클래스를 만들고 이 클래스 내부에 eng, math, sci 등 과목들의 점수를 저장했을 때, __next__() 메소드에서 미리 정한 순서에 따라 해당 속성값을 리턴하도록 한다면 Student 클래스의 인스턴스는 for ... in 문을 통해서 개별 과목의 점수를 순회할 수 있을 것이다.

+ Recent posts