본문 바로가기
[파이썬]/객체

[파이썬][객체] 이터레이터

by sung min_Kim 2023. 11. 17.
이터레이터

반복 가능한 객체를 생성하는 파이썬 객체


· 이터레이를 사용하는 이유는 ?


 이터레이터를 사용하는 주된 이유는 '지연 계산'을 가능하게 하기 위해서이다. 이는 파이썬의 이터레이터가 데이터를 처리하는 방식의 핵심 원칙이다.

 '지연 계산'은 데이터가 실제로 필요할 때까지 그 계산을 미루는 방법을 말한다. 이는 특히 대용량 데이터를 다루는 경우에 용이하다. 이터레이터를 사용하면, 모든 데이터를 미리 계산하고 메모리에 저장할 필요 없이, 데이터가 필요한 시점에만 그 값을 계산하기 때문이다. 이렇게 하면 메모리 사용량을 크게 줄일 수 있다. 또한, 데이터 스트림을 다루는 경우에도 용이하다.

 '데이터 스트림'이란 끝이 없는 연속적인 데이터의 흐름을 말하는데, 이터레이터를 사용하면 이러한 데이터 스트림을 효과적으로 처리할 수 있다. 이터레이터는 필요한 만큼의 데이터만을 추출하고 처리하므로, 무한한 데이터 스트림을 다루는 데에도 유용하다.

 따라서, 이터레이터의 주된 사용 이유는 대용량 데이터나 무한한 데이터 스트림을 효과적이고 효율적으로 다루기 위해서이다.

  • 메모리 효율성 : 값을 출력 시, 한 번에 하나의 요소만을 메모리에 등록하므로, 큰 데이터를 다룰 때 메모리를 효과적으로 사용할 수 있다.

  • 코드의 간결성 : 복잡한 루프 구조를 간단하게 만들 수 있으며, 코드를 보다 읽기 쉽고 관리하기 쉽게 만들 수 있다.

  • 무한한 시퀀스 : for, while 등의 반복문을 활용하여 무한한 시퀀스(리스트, 배열, 튜플 등)를 생성할 수 있다.

 


· 기본 형태

 

class My_Iterator:
    # 클래스 생성자 정의하기
    def __init__(self):
        
    # 자신의 클래스를 반환하는 iter 함수 정의
    def __iter__(self):
        return self
        
    # 반복을 수행하는 next 함수 정의
    def __next__(self):
	if문: 또는 for문:
	  result = 실행문
	  return result
	else :
  	  raise StopIteration

 

 

  • 이터레이터 : '__iter__()' 메소드와 '__next__()' 메소드를 가진 객체이다.

  • __init__() 메소드 : 클래스의 인스턴스가 생성될 때 호출되는 생성자 함수이다. 반복을 수행할 객체의 속성을 초기화하는 데 사용한다.


  • __iter__() 메소드 : 이터레이터 객체 자체를 반환함으로써, 해당 객체가 이터레이터임을 파이썬에게 알리는 함수이다. 이로써 이 메소드를 포함한 해당 객체(클래스의 인스턴스)는 반복문에서 사용할 수 있게 된다.

  • __next__() 메소드 : 이터레이터의 다음 요소를 하나씩 반환하는 함수이다. 이터레이터의 모든 요소를 순회한 이후에는 'StropIteration' 예외를 발생시켜 순회가 끝났음을 알린다.

 


· 동작 원리

 

class MyIterator:

    def __init__(self):
        self.current_value = 0
        
    def __iter__(self):
        return self
        
    def __next__(self):
        if self.current_value < 5:
            self.current_value += 1
            result = self.current_value
            return result
        else:
            raise StopIteration


 이터레이터 객체는 클래스를 정의하여, init, iter, next 함수를 선언하고, 'iter' 함수를 통해 객체 자신을 반환함으로써 이터레이터 객체임을 파이썬에게 알리고 있다. 이로써 반복문을 사용하여 본 객체(클래스의 인스턴스)의 각 요소들을 순회할 수 있게 된다.
(아래의 코드를 통해 객체로서 메모리에 할당되어 있는 것을 볼 수 있다.)

my_iter = MyIterator()
print(my_iter) # <__main__.MyIterator object at 0x7f12c94d47c0> 출력



 'init' 함수에서는 'current_value'를 객체로 할당하여 해당 속성값을 '0'으로 초기화하였다. 이로써 'iter' 함수에서는 해당 객체에 반복문을 사용하여 각 요소들을 순회할 수 있다.

 'next' 함수에서는 반복문을 통해 순회한 각 요소들을 얻는다. 각 요소들을 하나씩 반환하는데, 이는 앞서 설명한 '지연 계산' 특성 때문이다. 이는 이터레이터의 주요한 특성 중 하나이다. 데이터의 모든 요소를 미리 계산하지 않고 필요한 시점에만 각 요소를 계산하고 반환한다. 그리하여 'next' 함수를 호출할 때마다, 다음 요소를 계산하고 반환한다.

 위 예시의 경우 'self.current_value'를 if문을 통해 '0'부터 '4'까지 순회하며, 'next' 함수를 처음 호출할 경우에는 '1'을 반환한다. 반환한 이후에 해당 함수는 메모리에서 해제되지 않고 계속 참조되는 상태로 존재하며, 내부의 지역 변수 'result'는 메모리에서 해제된다. 따라서 'next' 함수는 현재 위치를 기억하고, 두 번째 호출되면 다음 요소인 '2'를 반환하게 된다.

print(next(my_iter)) # 1 출력
print(next(my_iter)) # 2 출력
print(next(my_iter)) # 3 출력
print(next(my_iter)) # 4 출력
print(next(my_iter)) # 5 출력



 모든 요소를 순회한 이후에는 'StopIteration' 예외를 발생시켜, 더 이상 순회할 요소가 없음을 알린다.

print(next(my_iter)) 
print(next(my_iter)) 
print(next(my_iter))
print(next(my_iter)) 
print(next(my_iter))
print(next(my_iter)) # StopIteration 오류 출력



 추가적으로 'try-except'를 사용하여 해당 오류를 예외처리 할 수도 있다.

try:
    print(next(my_iter)) # 1 출력
    print(next(my_iter)) # 2 출력
    print(next(my_iter)) # 3 출력
    print(next(my_iter)) # 4 출력
    print(next(my_iter)) # 5 출력
    print(next(my_iter)) # 이터레이터가 종료되었습니다 출력
except:
    print("이터레이터가 종료되었습니다.")

 


몇 가지 예시를 살펴보도록 하자.

 

-  문자열을 전달받아서 하나씩 추출하여 반환하는 이터레이터 생성
class StringIterator:
    def __init__(self, text):
        self.text = text
        self.index = 0
    
    def __iter__(self):
        return self
        
    def __next__(self):
        if self.index < len(self.text):
            result = self.text[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration
text = "Hello"
string_iter = StringIterator(text)


 생성자에서는 클래스의 인스턴스로 'text'와 'index'를 사용하고 있다. 'text'는 외부에서 전달받는 인자로서 순회할 대상이고, 'index'는 내부에서 사용하는 변수로서 현재 순회 중인 'text'의 위치를 가리킨다. 'index'는 초기값이 '0'으로 설정되어 있다.

 'next' 함수를 호출할 때마다, 'index' 위치에 존재하는 'text'를 반환하며, 'index'의 값은 1씩 증가한다. 따라서 'index'는 다음의 위치를 기억하고 'next' 함수를 한번 더 호출할 경우에는 다음 요소를 반환한다.

print(next(string_iter)) # H 출력
print(next(string_iter)) # e 출력
print(next(string_iter)) # l 출력
print(next(string_iter)) # l 출력
print(next(string_iter)) # o 출력



- 일반 프로그래밍 방식과 이터레이터 방식의 메모리 사용량 비교


 이터레이터는  값을 출력 시, 한 번에 하나의 요소만을 메모리에 등록하므로, 큰 데이터를 다룰 때 메모리를 효과적으로 사용할 수 있어 '메모리 효율성'이 좋다. 한번 확인해 보도록 하자.
(직접 따라 해 보실 분들을 위해 몇 가지 필요한 부분에 대해 아래 '더보기'에 작성해 두었으니, 참고하시길 바란다.)

더보기
  1. 메모리 확인을 위한 라이브러리 설치하기
    - 프롬프트를 실행하여 가상 환경에 들어간 뒤, 'pip install memory-profiler' 명령어를 입력하여 메모리 확인을 위한 라이브러리를 설치하도록 하자.
    라이브러리 설치 완료
  2.  설치한 라이브러리 불러오기
    - from memory_profiler import profile

  3. (주피터 노트북 환경이라면) 로드 처리하기
    %load_ext memory_profiler

 

from memory_profiler import profile

%load_ext memory_profiler

class SimpleIterator:
    def __init__(self, limit):
        #반복 범위를 지정할 값(반복의 끝값)
        self.limit = limit
        # 반복 시작값
        self.current = 0
        
    def __iter__(self):
        return self
        
    def __next__(self):
        if self.current < self.limit:
            self.current += 1  
            return self.current
        else:
            raise StopIteration


 동작을 100만 번 반복 수행하여, '이터레이터를 사용한 케이스(yes_iterator)'와 '사용하지 않은 케이스(no_iterator)'를 비교할 것이다.

# 이터레이터를 사용하지 않는 경우
@profile
def no_iterator(limit):
    data = [i for i in range(1, limit+1)]
    
# 이터레이터를 사용한 경우
@profile
def yes_iterator(limit):
    data = SimpleIterator(limit)
    for item in data:
        pass



 본인은 주피터 노트북 환경에서 코드를 실행하였으며, 파이썬 파일로 코드를 실행하실 분들은 '%memit' 키워드를 제외하고, 맨 상단에 'if __name__ == "__main__":' 명령어를 추가하여 동작하면 된다.
(웹의 주피터 노트북은 외부 자원 호출이 되지 않으므로, '%memit' 키워드를 사용하면 외부 자원 호출이 가능하게 한다.)

limit = 1000000
try:
    %memit no_iterator(limit)
    
    %memit yes_iterator(limit)
except:
    pass



 이터레이터를 사용하지 않는 경우, 총 메모리 사용량은 '98.40 MiB'로, '29.86 MiB'가 증가한 수치를 보인다.
 반대로 이터레이터를 사용한 경우, 총 메모리 사용량은 '70.77 MiB'로, '0.02 MiB'가 증가한 수치를 보인다.
 둘을 비교하여 보면, 이터레이터를 사용한 경우가 메모리 사용량이 월등히 적은 것을 볼 수 있다. 이처럼 이터레이터는 대용량의 데이터를 다루는데 매우 용이하다.
( peak memory  : 최대 사용된 메모리   |   increment : 메모리 증가 폭 )

# 이터레이터를 사용하지 않는 경우
ERROR: Could not find file C:\Users\vosej\AppData\Local\Temp\ipykernel_13480\2445230282.
py peak memory: 98.40 MiB, increment: 29.86 MiB

# 이터레이터를 사용한 경우
ERROR: Could not find file C:\Users\vosej\AppData\Local\Temp\ipykernel_13480\2445230282.
py peak memory: 70.77 MiB, increment: 0.02 MiB


 프롬프트에서도 메모리 사용량을 확인할 수 있다.
 명령어 : python -m memory_profiler 04_iterator_memory_test.py

이터레이터 사용 전후 메모리 사용량 비교



- 짝수값을 추출하는 이터레이터 생성

 

class EvenNumberIterator:
    def __init__(self, start_num, end_num):
        self.start_num = start_num
        self.end_num = end_num
    
    def __iter__(self):
        return self
        
    def __next__(self):
        for v in range(self.start_num, self.end_num):
            if self.start_num % 2 == 0:
                result = self.start_num
                self.start_num += 1
                return result
            else:
                self.start_num += 1
        raise StopIteration


 'start_num'과 'end_num'을 객체로 반환하여, 반복문을 통해 각 요소를 순회할 수 있도록 한다. 각 객체는 외부로부터 전달받아 사용한다.

 range() 함수를 이용하여 'start_num'부터 'end_num'까지의 리스트를 생성하여, 순회한 각 요소를 변수 'v'에 담는다. 이때, 시작값인 'start_num'가 짝수의 조건에 부합하면 리턴되고, '+ 1' 씩 증가되어 다음 요소를 순회한다. 모든 요소를 순회하고 나면 'StropIteration' 예외를 발생시켜 순회가 종료되었음을 알린다.

 'start_num'에 '1'을 대입하고, 'end_num'에 '10'을 대입하여, 이터레이터를 동작하면 각 요소들이 순차적으로 출력되는 것을 확인할 수 있다.

even_iter = EvenNumberIerator(1, 10)

for v in even_iter:
    print(v) # 2 4 6 8 출력



- 외부 함수를 이용하여 짝수값을 추출하는 이터레이터 생성

 

def is_even(num):
    return num % 2 == 0


 외부 함수 'is_even'에서  숫자가 짝수인 조건을 설정하였다. 해당 함수는 'EvenNumberIterator' 클래스의 매개 변수인 'func'의 인자로 전달되어 사용된다.


class EvenNumberIterator:
    def __init__(self, start, end, func):
        self.start = start
        self.end = end
        self.func = func

    def __iter__(self):
        return self
        
    def __next__(self):
        while self.start <= self.end:
            if self.func(self.start):
                result = self.start
                self.start += 1
                return result
            else:
                self.start += 1
        raise StopIteration

 
 해당 클래스에서는 'start', 'end', 'func'을 외부에서 전달받아, 객체로 반환하여 반복문을 통해 각 요소를 순회할 수 있도록 하였다. 이때, 변수 'func'는 외부 함수인 'is_even'을 참조한다.

 'next' 함수 내부에서는 시작값이 짝수인 경우에만 그 값을 반환하고, 시작값을 '1'씩 증가시켜 다시 호출될 경우에 다음 위치를 참조할 수 있도록 메모리에 남아있는 상태로 존재한다. 이를 통해 이터레이터는 값을 순차적으로 제공하고, 모든 요소를 순회하면 'StopIteration' 예외를 발생시켜 순회가 종료되었음을 알린다.


even_iter = EvenNumberIterator(1, 10, is_even)

for even in even_iter:
    print(even) # 2 4 6 8 10 하나씩 출력


 'start'에 '1'을, 'end'에 '10'을, 'is_even'에 ' is_even' 함수를 대입하여 이터레이터를 동작하면 순서대로 다음의 값들이 출력되는 것을 볼 수 있다.



- 텍스트 파일의 내용을 반환하는 이터레이터 생성

 

class FileLineIterator:
    def __init__(self, file_path):
        self.file_path = file_path
        self.file = open(file_path, "r", encoding='utf-8')
    
    def __iter__(self):
        return self
    
    def __next__(self):
        line = self.file.readline()
        if line:
            return line.strip()
        else:
            self.file.close()
            raise StopIteration


 읽어올 텍스트 파일의 경로는 외부에서 받아 사용할 것이고, 파일 경로와 파일은 객체로 반환하여 계속 참조할 수 있는 상태로 만든다. 'readline' 함수를 사용하여 파일에 입력된 텍스트 한 줄씩 읽어오고, 'strip' 함수를 사용하여 읽어온 텍스트의 공백을 제거하여 반환한다. 더 이상 읽어올 텍스트가 없다면 오픈한 파일을 닫고, 'StopIteration' 예외를 처리하여 이터레이터가 종료되었음을 알린다.


 객체로 전달할 파일 경로는 현재 사용하고 있는 주피터 노트북의 경로와 동일하게 설정하였다.
(본인은 읽어올 텍스트 파일 '04_example.txt'를 미리 생성하였다.)

file_path = "./04_example.txt"
file_iter = FileLineIterator(file_path)



 출력 결과, 텍스트 파일에 작성된 첫 줄부터 네 번째 줄 까지 순서대로 출력되는 것을 확인할 수 있다.

for line in file_iter:
    print(line) # 텍스트 파일에 입력한 "한줄 두줄 세줄 네줄" 출력

 


이터레이터에 대하여 알아보았다.

이터레이터는 '지연 연산' 방식을 사용하여, 필요한 시점에 요소를 생성하고, 반복문을 통해 그 요소들을 순차적으로 한 번에 하나씩 반환하는 객체이다.


이 방식을 통해 메모리를 효율적으로 관리하며, 이는 대용량 데이터를 다루는 상황에서 이터레이터의 중요성을 이해하는 데 도움이 될 것이다.

 

'[파이썬] > 객체' 카테고리의 다른 글

[파이썬][객체] 제너레이터  (0) 2023.11.17