본문 바로가기
프로그래밍 언어/Python

[Python] 제너레이터

by 토마토베이컨수프 2021. 11. 11.

제너레이터를 사용하는 이유

제너레이터는 왜 사용하는 걸까요? 제너레이터를 사용하면 이터러블(iterable) 객체를 생성하여 큰 데이터 집합의 구성요소를 한 번에 하나씩 반환하게 하여, 메모리를 절약할 수 있습니다. 따라서 좀 더 유연하고 메모리 효율적인 코드를 작성할 수 있게 되죠.

제너레이터는 이터레이터이므로 먼저 이터레이터에 대해 간단히 알아본 후, 제너레이터의 사용 예시에 대해 알아보겠습니다.

 


이터레이터(iterator)

이터레이터는 next 함수 호출 시 계속해서 그 다음 값을 리턴해주는 객체입니다.

 

리스트를 iter 함수로 감싼 후 간단한 이터레이터를 만들 수 있습니다.

lst = [1, 2, 3]
lst = iter(lst)

next(lst)	# 1
next(lst)	# 2
next(lst)	# 3
next(lst)	# StopIteration

next() 내장 함수는 이터레이터를 다음 요소로 이동시키고 기존의 값을 반환하는 함수인데요, 이처럼 next 함수 호출 마다 이터레이터의 요소값들이 차례대로 리턴되고 한계치를 넘으면 StopIteration 에러가 발생하게 됩니다. 이터레이터는 next로 그 값을 읽어 들이면 다시 그 값을 읽을 수 없는 특징이 있습니다.

 

이와 같은 이터레이터의 특성은 클래스 내부의 __next__라는 메소드에 의해 나타는건데요, 클래스로 이터레이터를 만드는 객체를 구현해보겠습니다.

class MyIterator:
    def __init__(self, number):
        self.number = number
        self.count = 0
        
    def __next__(self):
        self.count += 1
        return self.count + self.number
        
obj = MyIterator(10)
next(obj)	# 11
next(obj)	# 12
next(obj)	# 13

 

그러나 위처럼 만든 이터레이터는 이터러블(iterable)하지 않습니다. for 루프로 이터레이터를 작동시킬 수가 없다는 얘기죠. 

이터레이터를 이터러블하게 만들어 주기 위해선 내부에 __iter__메소드를 만들어 줘야 합니다.

class MyIterator:
    def __init__(self, number):
        self.number = number
        self.count = 0
        
    def __iter__(self):
        return self
        
    def __next__(self):
        self.count += 1
        return self.count + self.number
        
obj = MyIterator(10)
for i in obj:
    print(i)	# 11 12 13 14 ...

 

정리하자면, 객체를 이터러블하게 만들어주는 메소드는 __iter__고, 이터레이터로 만들어주는 메소드는 __next__인 것입니다. 그리고 앞서 말한 이터레이터의 특성대로, for문을 이용하여 요소값을 반복하고 난 후에는 다시 반복하더라도 더 이상 그 값을 다시 가져오지 못합니다.

 


제너레이터(generator)

이제 제너레이터에 대해 알아봅시다. 제너레이터는 함수가 마치 이터레이터처럼 하나의 값만을 리턴하는게 아닌 연속된 값들을 순차적으로 리턴할 수 있게 해줍니다. 

제너레이터를 사용하려면 클래스를 만들지 않고, 단지 return 대신 yield를 사용하는 함수를 만들면 됩니다.

def gen(start):
    while True:
        yield start
        start += 1
    
g = gen(1)
next(g)		# 1
next(g)		# 2
next(g)		# 3

 

제너레이터 특성을 사용한 함수 내에서 yield 키워드를 만나게되면, 그 값을 리턴하고 현재 상태를 기억한 채로 다른 값을 요청할 때까지(다른 yield문에 도달할 때까지) 실행을 일시 중지합니다. 따라서 위 코드처럼 무한 루프를 사용해도 안전한 것이죠.

이처럼 모든 값을 한번에 계산하지 않고 필요할 때마다 하나의 항목씩 수행하는 제너레이터의 특성을 "느긋한 계산법(Lazy evaluation)" 이라고 부릅니다.

 

그렇다면 이제 제너레이터를 통해 큰 데이터 파일에서 작업에 필요한 데이터만 가져와 메모리를 절약할 수 있는 코드를 작성해보겠습니다.

def read_in_chunks(file_handler, chunksize=1024):
    while True:
        data = file_handler.read(chunksize)
        if not data:
            break
        yield data
        
f = open("large_data.dat")
for piece in read_in_chunks(f):
    print(piece)

 

함수를 사용하지 않고 좀 더 간단하게 제너레이터를 만들 수도 있습니다. 제너레이터 표현식이라 하여 작성된 이 구문은 리스트 컴프리핸션과 비슷하지만, 리스트 대신 튜플을 이용한다는 점이 다릅니다.

gen = (i * i for i in range(1, 1000))

gen = mygen()
print(next(gen))	# 1
print(next(gen))	# 4
print(next(gen))	# 9

 

yield from는 다른 제너레이터에서 값을 얻는 역할을 합니다. 이 키워드를 이용하면 다음과 같이 멀티 리스트를 펼치는 코드를 작성할 수도 있습니다.

def flat_list(lter_values):
    for item in iter_values:
        if hasattr(item, "__iter__"):
            yield from flat_list(item)
        else:
            yield item
            
list(flat_list([1, [2], [3, [4]]]))	# [1, 2, 3, 4]

 


참고 자료

  • 파이썬 라이브러리 - 이터레이터와 제너레이터 : https://wikidocs.net/134909
  • <클린 파이썬 : 효과적인 파이썬 코딩 기법, 수닐 카필 지음>
  • <파이썬 클린 코드 : 유지보수가 쉬운 파이썬 코드를 만드는 비결, 마리아노 아나야 지음>

'프로그래밍 언어 > Python' 카테고리의 다른 글

[Python] 데코레이터  (0) 2021.11.21
[Python] 클래스 구조  (0) 2021.11.16
[Python] 유용한 데이터 구조  (0) 2021.11.07
[Python] 파이썬 다운 코딩  (0) 2021.11.05
[Python] SOLID 원칙  (0) 2021.10.03