본문 바로가기

python

python - iterator, generator(이터레이터와 제네레이터)

출처 : https://nvie.com/posts/iterators-vs-generators/

컨테이터(container)
컨테이너는 원소들을 가지고 있는 데이터 구조이며 멤버쉽 테스트를 지원합니다. 이는 메모리에 상주하는 데이터 구조로, 보통 모든 원소값을 메모리가 가지고 있습니다. 파이썬에서 잘 알려진 컨테이너는 다음과 같습니다. 
기술적으로 어떤 객체가 특정한 원소를 포함하고 있는지 아닌지를 판단할 수 있으면 컨테이너라고 합니다. 다음과 같이 리스트, 셋, 튜플에 대해 멤버쉽 테스트를 할 수 있습니다.

 

assert 1 in [1, 2, 3]     # lists
assert 4 not in [1, 2, 3]
assert 1 in {1, 2, 3}     # sets
assert 4 not in {1, 2, 3}
assert 1 in (1, 2, 3)     # tuples
assert 4 not in (1, 2, 3)

 

딕셔너리 멤버쉽은 키 값을 체크합니다. 

 

d = {1: 'foo', 2: 'bar', 3: 'qux'}
assert 1 in d
assert 4 not in d
assert 'foo' not in d  # 'foo'는 딕셔너리의 키값이 아니다

 

마지막으로 문자열에는 부분문자열이 '포함'되어 있는지를 체크할 수 있습니다.

 

s = 'foobar'
assert 'b' in s
assert 'x' not in s
assert 'foo' in s  # 문자열은 부분문자열을 모두 "포함"하고 있다

 

마지막 예제는 조금 이상하지만 이는 컨테이너 인터페이스가 어떻게 불투명하게 객체를 렌더링하는지를 보여줍니다. 문자열은 모든 부분문자열들의 리터럴 복사본을 메모리에 저장하고 있지는 않지만 의심의 여지 없이 위와 같이 사용할 수 있습니다. 

대부분의 컨테이너가 자신이 포함하고 있는 모든 원소들을 생성하는 방법을 제공하지만 이 기능은 이를 컨테이너로 만드는게 아니라 이터러블로 만듭니다.
모든 컨테이너가 이터러블 할 필요는 없습니다. 이의 한 예는 Bloom filter입니다. 이와 같은 확률적 데이터 구조는특정 원소를 포함하고 있는지는판단할 수 있지만 각각의 개별 원소는 반환하지 못합니다.

https://en.wikipedia.org/wiki/Bloom_filter


이터러블(iterable)
대부분의 컨테이너는 또한 이터러블합니다. 그러나 더 많은 것들이 또한 이터러블합니다. 일례로 파일 열기, 소켓 열기 등등이 있습니다. 컨테이너가 일반적으로 유한할 경우, 이터러블은 무한한 데이터 소스를 나타낼 수도 있습니다. 
이터러블은 반드시 데이터 구조일 필요는 없으며 이터레이터(모든 원소를 반환할 목적으로)를 반환할 수 있는 모든 객체가 가능합니다. 이터러블과 이터레이터는 중요한 차이점이 있습니다. 예시를 통해 알아보겠습니다. 

 

x = [1, 2, 3]
y = iter(x)
z = iter(x)
next(y)
 > 1
next(y)
 > 2
next(z)
 > 1
type(x)
 > <class 'list'>
type(y)
 > <class 'list_iterator'>

 

여기서 y와 z는 각각 이터러블 x로부터 값을 생성해내는 이터레이터의 인스턴스이고 x는 이터러블입니다. y와 z는 예시에서 볼 수 있듯이 상태를 가집니다. 이 예시에서 x는 데이터 구조(리스트)이지만 이는 필수 조건은 아닙니다. 

종종 실용적인 이유로 이터러블 클래스는같은 클래스에 iter()와 next()를 모두 구현하며 클래스를 이터러블과 자체 이터레이터로 만들어주는 self를 반환하는 iter()를 갖습니다. 그러나 이터레이터로 다른 객체를 반환해도 상관 없습니다.

다음과 같이 코드를 작성하면 

 

x = [1, 2, 3]
for elem in x:
    ...

 

이러한 일이 일어납니다.

https://mingrammer.com/translation-iterators-vs-generators/

파이썬 코드를 디스어셈블링(어셈블리 수준으로 코드를 해부함) 해보면iter(x)를 실행시키는데 필요한 GET_ITER를 호출하고 있음을 알 수 있습니다. FOR_ITER는 모든 원소를 반복적으로 가져오기 위해 next()를 호출하는 것과 동일한 일을 수행하지만 인터프리터에서 속도에 최적화 되어있기 때문에 바이트 코드 명령어에서는 잘 사용하지 않습니다. 


이터레이터(iterator)
이터레이터는 next()를 호출할 때 다음 값을 생성해내는 상태를 가진 헬퍼 객체입니다. next()를 가진 모든 객체는 이터레이터입니다. 값을 생성해내는 방법과는 무관합니다. 즉 이터레이터는 값 생성기입니다. "다음" 값을 요청할 때마다 내부 상태를 유지하고 있기 때문에 다음값을 계산하는 방법을 알고있습니다. 이터레이터의 예시는 정말 많습니다. itertools의 모든 함수는 이터레이터를 반환합니다. 일부는 무한 시퀀스를 생성합니다. 
이터레이터 내의 상태는 prev와 curr 인스턴스 값으로 유지되고 있으며 이터레이터를 호출하는 서브 시퀀스에 사용됩니다. next()를 호출할때마다 두가지 중요한 작업이 수행됩니다. 
 1. 다음 next() 호출을 위해 상태를 변경한다. 
 2. 현재 호출에 대한 결과값을 생성한다. 

제네레이터(generator)

제네레어타는 반복자(iterator)와 같은 루프의 작용을 컨트롤하기 위해 쓰여지는 특별한 함수 또는 루틴이다. 사실 모든 제네레이터는 반복자이다. 제네레이터는 배열이나 리스트를 리턴하는 함수와 비슷하며, 호출을 할 수 있는 파라메터를 가지고 있고, 연속적인 값들을 만들어 낸다. 하지만 한번에 모든 값을 포함한 배열을 만들어서 리턴하는 대신에 yield 구문을 이용해 한 번 호출될 때마다 하나의 값만 리턴하고 그런 이유로 일반 반복자에 비해 아주 작은 메모리를 필요로 한다. 간단히 얘기하면 제네레이터는 반복자와 같은 역할을 하는 함수이다.
- 위키피디아 -

일반 함수가 호출되면 코드의 첫 번째행 부터 시작하여 리턴(return) 구문이나, 예외(exception) 또는 (리턴을 하지않는 함수이면) 마지막 구문을 만날때까지 실행된 후, 호출자(caller)에게 모든 컨트롤을 리턴합니다. 그리고 함수가 가지고 있던 모든 내부 함수나 모든 로컬 변수는 메모리상에서 사라집니다. 같은 함수를 다시 호출하게 되면 모든 것은 처음부터 다시 새롭게 시작됩니다. 
그런데 프로그래머들은 한번에 일을 다하고 영원히 사러져버리는 함수가 아닌 하나의 일을 마치면 자기가 했던 일을 기억하면서 대기하고 있다가 다시 호출되면 전의 일을 계소기 이어서 하는 함수를 필요로 하기 시작했습니다. 그래서 만들어진 것이 제네레이터입니다. 제네레이터를 사용하면 일반 함수보다 훨씬 좋은 퍼포먼스를 낼 수가 있고, 메모리 리소스도 절약할 수 있습니다. 

 

def numbers(nums):
   result = []
   for i in nums:
      result.append(i * i)
mynums = numbers([1,2,3,4,5])
print(mynums)
 > [1,4,9,16,25]

 

이 코드는 리스트를 조작하는 간단한 함수를 호출하고 있습니다. 위와 같은 코드를 제네레이터로 만들면 다음과 같습니다. 

 

def numbers(nums):
   for i in nums:
      yield i * i
mynums = numbers([1,2,3,4,5]) ####
print(mynums)
 > <generator object numbers at 0x1007c8f50>

 

결과값을 확인해보니 제네레이터라는 오브젝트가 반환되는 것을 볼 수 있습니다. 제네레이터는 자신이 리턴할 모든 값을 메모리에 저장하지 않기 때문에 조금 전 일반 함수의 결과와 같이 한번에 리스트로 보이지 않는 것입니다. 제네레이터는 한 번 호출될떄마다 하나의 값만 전달(yield)합니다. 즉, ####위치까지는 아직 아무런 계산을 하지 않고 누군가가 다음 값에 대해서 물어보기를 기다리고 있는 상황입니다. 

 

def numbers(nums):
   for i in nums:
      yield i * i
mynums = numbers([1,2,3,4,5])
print(next(mynums))
print(next(mynums))
print(next(mynums))
 > 1
 > 4
 > 9

 

next()함수를 사용해 다음 값을 출력할 수 있다는 것을 보았습니다. 이렇게 next()를 호출하여 값들을 차례대로 얻을 수 있습니다. 제네레이터가 가지고 있는 갯수만큼의 값 이상을 요구하면 StopIteration예외가 발생합니다. 일반적으로 제네레이터는 리스트와 같이 for문을 이용하여 값을 추출합니다. for문을 이용하여 결과를 보면 그냥 리스트를 출력할때와 같은 결과로 보이기 때문에 혼동할 수 있습니다. 

 

def numbers(nums):
   for i in nums:
      yield i * i
mynums = numbers([1,2,3,4,5])

for num in mynums:
   print(num)
 > 1
 > 4
 > 9
 > 16
 > 25

 

그런데 문제가 있습니다. 제네레이터는 모든 값을 가지고 있지 않고 한번에 하나의 값만을 출력합니다. 그리고 그 값을 다 출력한 뒤에는 더이상 값을 가지고 있지 않고 출력하려고 할 시에는 StopIteration이라는 예외를 발생시킵니다. 다음의 코드를 보면서 살펴보겠습니다. 

 

####################################################
# iter.txt

1234
345
346
4
56
####################################################
iter.py

def normal(numbers):   
   total = sum(numbers) # 이터레이터가 합계를 구하면서 다 소진됨
   result = []
   for value in numbers:
      percent = 100 * value / total
      result.append(percent)
   return result

def read(path):
     with open(path, 'r') as f:
      for line in f:
         yield int(line)

if __name__ == '__main__':
   path = 'iter.txt'
   it = read(path) # path에 있는 파일을 읽어서 제네레이터 생성
   read_it = normal(it) # 리스트 생성
   print(read_it)
####################################################
# 결과
 > []

 

위의 코드를 실행한 결과는 리스트형의 빈 값이 출력됩니다. 그 이유는 제네레이터는 상태가 있기 때문입니다. 이런 함수는 예외를 일으키지 않아 결과가 없는 데이터와 결과가 있지만 이미 소진한 이터레이터의 상태를 알려주지 않습니다. 데이터를 확인해본 결과 normal()함수의 가장 첫 부분에서 합계를 구하는 sum()함수가 있습니다. 이 함수는 iterable한 객체에만 사용되며 그 말은 객체의 값을 순회한다는 것입니다. 파일에서 읽어온 값들은 이 함수에서 iterator를 돌며 다 소진되었습니다. 그러므로 normal()함수 하단에서 값을 리스트로 만들기도 전에 이미 비어있는 것입니다. 그렇다면 어떻게 해야 할까요? 

방법은 다시한번 생성하는 것입니다. 파이썬의 함수는 일급시민으로써 인자로 활용이 될 수 있습니다. 코드를 수정해보겠습니다. 

 

def normal(iter):   
   total = sum(get_iter()) # 이터레이터 생성
   result = []
   for value in get_iter(): # 새로운 이터레이터 생성
      percent = 100 * value / total
      result.append(percent)
   return result

def read(path):
     with open(path, 'r') as f:
      for line in f:
         yield int(line)

if __name__ == '__main__':
   path = 'iter.txt'
   it = read(path) # path에 있는 파일을 읽어서 제네레이터 생성
   read_it = normal(lambda: read(path)) # 람다를 사용하여 함수에 파라미터를 넣어 인자로 넘김
   print(read_it)

 

위와같은 방법으로 제네레이터인 함수를 파라미터로 넘겨 그때그떄 생성하여 사용하는 것입니다. 

'python' 카테고리의 다른 글

python - 패킹과 언패킹(packing & unpacking)  (0) 2020.04.15
python - format(문자열)  (0) 2020.04.09
python - 재귀 함수  (0) 2020.04.08
python - 반복문(break, continue)  (0) 2020.04.07