본문 바로가기

Program Languages/Python

파이썬 iterable, iterator, generator에 대해 알아보자

이번 포스트에서는 iterable과 iterator에 대해 알아보려고 합니다.


우선 Iterable이란 어떠한 일련의 데이터에 하나씩 접근할 수 있는 객체를 나타내는 단어이며,

 

이는 추상적인 일종의 클래스 함수 헤더(e.g. 자바의 인터페이스)로 클래스 내부에서 __next__나 __iter__ 혹은 (__getitem__ , __len__) 함수가 구현되어 있다면 그것을 iterable한 클래스라고 명명할 수 있습니다.

더보기

__aiter__ 이나 __anext__ , __await__ 등도 있지만 이 포스트에서는 생략하도록 하겠습니다.

 

즉, 어떤 파이썬 클래스가 iterable한 속성을 가지기 위해서는 클래스 내부에서

  • __next__ 함수가 구현되어 있거나
  • __iter__ 함수가 구현되어 있거나
  • __getitem__ 함수 __len__ 함수가(시퀀스로) 구현되어 있거나

이 3가지 중 한가지 조건을 충족한다면 해당 함수가 구현된 클래스는 iterable하다 라고 할 수 있습니다.


 

 

Iterator란 어떤 연속되는 일련의 데이터를 나타내는 객체로서, __iter__ 함수가 구현되어 있거나 __iter__함수를 대체할 수 있는 함수로 구현된 클래스를 iterator라고 부릅니다.

 

조금 더 정확하게 표현하면, iterator는 파이썬의 기본적으로 정의된 함수인  iter()함수로 불려질 수 있는 클래스를 iterator라고 부릅니다.


 

파이썬의 Generator란 Iterator와 유사하나, return <value> 코드 대신 yield <value> 라는 코드를 사용하여 어떠한 연속적인 일련의 데이터를 만들어가는 행위입니다.

 

 

이때, 파이썬의 이러한 Generator를 사용해 만들어진 iterator를 Generator Iterator라고 부릅니다.

 


 

 

위의 정리를 살펴보면 이 무슨 말장난인가라고 생각하시는 분들이 있을겁니다.

 

조금 더 쉽게 일반 Iterator와 Generator Iterator의 차이를 예를 들어 언급하자면,

  • 일반 Iterator는 어떤 주문한 물건을 어떠한 순서에 따라 즉시 지급하는 형태이다.
  • Generator Iterator는 어떤 주문할 물건을 어떠한 순서에 따라 일단 사전에 전체적으로 대기표를 받고 각 주문 순서가 오면 그때 물건을 지급하는 형태이다.

라고 생각하시면 좋을 것 같습니다.

 

여기서 물건이라고 예시를 했지만, 위의 예시의 물건은 함수에 의한 계산이나 처리된 결과등을 의미하고, 지급한다는 예시는 '결과에 필요한 리소스를 할당한다'라고 대입하시면 됩니다.

 

즉,한줄로 정리하면, 일반 Iterator는 순서에 따라 즉시 물건을 지급받고, Generator Iterator는 일단 순번 대기표를 받은 후 대기 순서가 왔을 때 그때 가서 물건을 지급하는 형태라고 생각하시면 됩니다.

 


 

iterable, iterator, generator, generator 가 무엇인지 간략히 알아보았으니,구현 예시를 언급하기에 앞서 어떤 클래스가 iterable를 속성을 가지는지 확인하는 코드를 살펴보겠습니다.

 

이를 확인하기 위해서 hasattr이라는 함수를 사용할 것인데, hasattr에 대한 함수 파라메터 를 언급하면 hasattr(확인할_클래스_인스턴스변수명, 선언되어_있는지_확인할_함수명_혹은_클래스_내부_변수명의_이름_문자열)->bool 입니다.

 

def hasNextMethod(obj):
    return hasattr(obj,"__next__")
def isOneOfIterators(obj):
    return (hasattr(obj,"__iter__") or 
    (hasattr(obj,"__getitem__") and hasattr(obj,"__len__"))
    )
def hasIterable(obj):
    return (hasNextMethod(obj) or isOneOfIterators(obj))

a=[1,2,3]
print(f"a is ({type(a)}){a}.\n\tDoes a have iterable? {hasIterable(a)}")
print(f"\tDoes a have next method?{hasNextMethod(a)}")
print(f"\tIs a one of iterators?{isOneOfIterators(a)}")
print()

a=1
print(f"a is ({type(a)}){a}.\n\tDoes a have iterable? {hasIterable(a)}")
print(f"\tDoes a have next method?{hasNextMethod(a)}")
print(f"\tIs a one of iterators?{isOneOfIterators(a)}")
print()

a=range(10,15)
print(f"a is ({type(a)}){a}.\n\tDoes a have iterable? {hasIterable(a)}")
print(f"\tDoes a have next method?{hasNextMethod(a)}")
print(f"\tIs a one of iterators?{isOneOfIterators(a)}")
print()

확인해보면 리스트와 range 데이터형은  iterator이나 next함수를 지원하지 않고 int데이터형은 iterable이 아닌 것을 확인할 수 있었습니다.


 

클래스에 대한 좀더 자세한 내용은 클래스관련 포스팅에서 다루도록 하겠습니다.

 

아래는 커스텀 클래스를 사용해 iterator를 구현하는 예제입니다.

class NumberPrinter:
    def __init__(self,n):
        self.n = n
        self.index = 0
    
    def __getitem__(self,index):
        if(self.index==self.n):
            #stop iteration 에러가 발생시 파이썬 해석기는 확인차인지 자체적으로 이후 stopiteration를 한번 더 발생시킴
            #따라서 해당 예제의 경우 if문 조건이 self.index>self.self.n인 경우에는 에러가 발생해 프로세스가 중단됨
            #따라서 위의 if문 조건처럼 하거나 코드 보완이 필요
            raise StopIteration
        print("(__getitem__)",end="")
        self.index+=1
        return index
    def __len__(self):
        return self.n
    def __calc(self,a,b):
        print("(__calc)",end="")
        return a+b
    
    def __iter__(self):
        for i in range(self.n):
            yield self.__calc(i,0)#with pythongenerator
    
    def __next__(self):
        if(self.index+1>=self.n):
            raise StopIteration
        value = self.index
        self.index+=1
        return value
    


def main():
    np = NumberPrinter(10)
    print("*"*3,"iter","*"*3)
    print(iter(np))
    for i in np:#iter(np):
        #for문은 내부적으로 iter함수를 불러오므로 for문에서는 iter 함수를 생략가능
        print(i,end=" ")
    print()
    print()

    print("*"*3,"next","*"*3)
    np2 = NumberPrinter(11)
    while(True):
        try:
            print(next(np2),end=" ")
        except StopIteration:
            break
    print()
if(__name__=="__main__"):
    main()

실행 결과는 아래와 같습니다.

 


 

여기서 next함수를 실행해야 하는데 만약 NumberPrinter클래스의 __next__함수가 없다면 다음과 같은 에러가 발생합니다.

또한 __iter__ 함수나 __getitem__함수가 구현되어 있지 않다면 다음과 같은 에러가 발생합니다.


 

또한 위의 실행결과를 보시면 print("(__getitem__)",end="") 부분이 출력이 안된것을 확인하실 수 있습니다.

이것을 통해 __iter__함수가 구현되어 있다면 __getitem__보다 __iter__함수를 우선시 한다는 가정을 세워볼 수 있습니다.

따라서 __iter__을 구현하지 않은 코드로 돌려 확인해보겠습니다.

더보기

코드 중복이지만 어떤 코드로 돌렸는지 재차 확인하기 위해 따로 적어두었습니다.

(... 으로 생략해둔 부분은 생략된 코드와 위의 코드사이의 같음을 의미합니다)

class NumberPrinter:
    def __init__(self,n):
        self.n = n
        self.index = 0
    
    def __getitem__(self,index):
        if(self.index==self.n):
            #stop iteration 에러가 발생시 파이썬 해석기는 확인차인지 자체적으로 이후 stopiteration를 한번 더 발생시킴
            #따라서 해당 예제의 경우 if문 조건이 self.index>self.self.n인 경우에는 에러가 발생해 프로세스가 중단됨
            #따라서 위의 if문 조건처럼 하거나 코드 보완이 필요
            raise StopIteration
        print("(__getitem__)",end="")
        self.index+=1
        return index
    def __len__(self):
        return self.n
    def __calc(self,a,b):
        print("(__calc)",end="")
        return a+b

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

 

위의 결과로 보아 __iter__ 함수가 구현되지 않고 __getitem__ 함수가 구현되면 , __getitem__ 함수로 동작된다 라고 할 수 있습니다.

 

 

이때 __iter__ 과 __getitem__ 함수의 큰 차이는

  • __getitem__ 은 시퀀스 형식의 일종으로 구현되 일반 Iterator로써 동작하고
  • __iter__ 은 iterator이지만 현재 구현상 Generator Iterator로 동작한다

인 것이 현재의 큰 차이점이라고 할 수 있습니다.

 

 


 

이것을 통해,

 

iter함수를 사용할 때는,

  1. __iter__가 구현되어 있는 경우에는  1순위로 __iter__ 의 구현 코드로 실행된다.(이때 StopIteration은 굳이 사용안해도 된다)
  2. __getitem__은 __iter__이 구현되어 있지않은 경우에 일종의 시퀀스 iterator 구현 코드로 실행된다.
  3. __iter__ 이나 __getitem__ 이 구현되어 있지 않다면 에러가 발생한다.

next함수를 사용할 때는 

  • __next__ 함수가 클래스 내부에 구현되어 있어야 동작한다.
  • next의 마지막은 stopiteration으로 일련의 데이터의 마지막부분이라고 알려줘야 제대로 동작한다.

 

로 정리를 해볼 수 있습니다.