IT/언어

[python] Iterator와 Generator

개발자 두더지 2020. 5. 13. 11:27
728x90

Python의 iterator과 generator에 대해 정리해보았다.

 

● iterator : 요소가 복수인 컨테이너(리스트, 퓨플, 셋, 사전, 문자열)에서 각 요소를 하나씩 꺼내 어떤 처리를 수행할 수 있도록 하는 간편한 방법을 제공하는 객체이다.  

● generator : iterator의 한 종류로, 하나의 요소를 꺼내려고 할 때마다 요소 generator를 수행하는 타입으로, Python에서는 yield문을 통해 구현하는 것이 보통이다.

 

python의 내장 컬렉션 (list, tuple, set, dic 등)의 어떤 것이든 iterable을 상속받고, 내장의 컬렉션을 사용한 반복문 처리로 미리 컬렉션 값을 입력해둬야할 필요가 있으므로 아래와 같은 경우는 interator또는 generator을 직접 구현하고 싶다고 생각하는 경우가 있을 것이다. 

● 무한히 반복하는 iterator

● 모든 요소를 미리 계산하는 것이 혹은 가져 오는 것이 계산 비용, 처리 시간, 메모리 사용량 등의 측면에서 힘든 경우

 

iterator와 generator의 관계도

(cf) 각 자료구조의 iterable, Generator, Iterator 상속 여부

list는 Iterable를 상속 받나요? True
list는 Iterator를 상속 받나요? False
list는 Generator를 상속 받나요? False
tuple는 Iterable를 상속 받나요? True
tuple는 Iterator를 상속 받나요? False
tuple는 Generator를 상속 받나요? False
set는 Iterable를 상속 받나요? True
set는 Iterator를 상속 받나요? False
set는 Generator를 상속 받나요? False
dict는 Iterable를 상속 받나요? True
dict는 Iterator를 상속 받나요? False
dict는 Generator를 상속 받나요? False

1. Iterable과 Iterator의 개념

 

 Iterable과 Iterator는 ‘collections.abc’ 내장 모듈에 정의되어 있는 추상 클래스로, 두 클래스는 python의 Iterator Protocol이라는 개념 속에서 정의되는 한 쌍이다.

 

1) Iterable Protocol

 Iterable과 Iterable를 개념적으로, 또는 실제적으로 구현하는 규칙을 정의한다. 쉽게 말하면, Iterable과 Iterator를 만드는 방법이라고 생각해도 된다. 이 프로토콜이 요구하는 대로 우리가 사용하는 모든 Iterable과 Iterator 클래스가 구현되어 있다. 즉, 앞서 살펴봤듯이 list, dict 등은 내부적으로 Iterator Protocol에서 정의한 Iterable 구현 요구사항이 모두 충족되어 있기 때문에 Iterable이다. 이 말은 이 프로토콜을 준수한 클래스를 만든다면, (dict, list 등 미리 정의된 것이 아닌) 우리만의 커스터마이즈한 Iterable과 Iterator를 만들 수 있다는 뜻이기도 하다. 

 

2) Iterable

 Iterable은 순회할 수 있는 모든 객체를 가리킨다. 다른 말로 하면 파이썬에서 for 문의 in 키워드 뒤에 올 수 있는 모든 값은 Iterable이다. 그러면 list, tuple, set, dict는 말할 것도 없고 문자열, 파일 등도 Iterable이라고 할 수 있다. 그러면 Iterator protocol을 통해 Iterable을 만들려면 어떻게 해야 할까?

 Iterable를 상속받는 클래스는, 즉 클래스가 Iterable이기 위해서는:

  • __iter__ 추상메소드를 실제로 구현해야 하며 이 메소드는 호출될 때마다 새로운 Iterator를 반환해야 한다.

자료구조나 클래스가 Iterable이기 위해서는 이 조건만 만족하면 된다. 즉, 클래스에 __iter__ 메소드가 구현되었으면 해당 객체에 iter 라는 내장 함수를 적용해 Iterator를 생성할 수 있다.

 

3) Iterator

 ‘Iterable은 for 문에 넣을 수 있는 모든 값’으로 정의하면 되지만 Iterator는 조금 더 까다롭다. Iterator는 상태를 유지하며 반환할 수 있는 마지막 값까지 원소를 필요할 때마다 하나씩 반환하는 것이라고 생각하면 된다.  ‘반환할 수 있는 마지막 값까지 원소를 하나씩 반환한다’의 의미는 가령 list에서 값을 하나씩 반환하는 것과 동작이 같다.

l = [1, 2, 3, 4, 5]

for n in l:
    print(n)

1
2
3
4
5

 for 문을 통해서 각 값을 하나씩 반환했다. 그리고 끝에 다다르면(여기서는 5) 더 이상 반환하지 않는다. 여기까지는 Iterable과 다르지 않는데 ‘상태를 갖는다’가 중요하다. 즉, '각 Iterator는 상태를 갖는다.' 이 말이 중요하다. Iterable에 iter 함수를 쓸 때마다 새로운 이터레이터가 생성된다. 이때 각 Iterator는 서로 다른 상태를 유지하고 있다. 다시 말해 한 Iterator의 동작이 다른 Iterator의 동작에 영향을 미치지 않는다.

 그러면 각 Iterator가 관리하는 ‘상태’란 무엇인가? 많은 사항이 있겠지만, 여기서는 각 Iterator가 순회하고 있는 위치라고 생각하면 된다. for 문을 통해서는 무조건 Iterable의 끝값까지 모두 살펴보지만, 뒤에서 확인가능하듯이 Iterator는 필요에 따라 한 번에 한 값씩만 반환할 수 있다.

 즉, 한 Iterable에 대한(가령 [1, 2, 3, 4]) 서로 다른 Iterator를 만들어서 한 Iterator는 2까지 반환해서 다음 반환값이 3에 머무르게 하고, 다른 Iterator는 4까지 모두 반환해서 더 이상 반환할 값이 없도록 할 수 있다는 것이다. 두 Iterator는 서로 다른 상태값을 유지, 관리하는 완전히 다른 객체다. 비록 같은 Iterable에서 생성됐지만. 이게 개념적 핵심이다.

 Iterator는 iter 함수의 인자로 Iterable을 적용해 반환된 객체로, 값을 하나씩 반환하며 그 상태값을 유지, 관리하는 객체라고 개념적으로 이해할 수 있었다. 그러면 이번엔 Iterator를 어떻게 구현하는지 살펴보자. Iterator protocol을 통해 Iterator를 만들려면 어떻게 해야 할까?  __abstractmethods__ 속성을 통해 살펴보자.

print(Iterator.__abstractmethods__)

frozenset({'__next__'})

클래스가 Iterator이기 위한 필수적인 메소드는 __next__인 것 같다.

어떤 클래스가 Iterator이기 위해서는 다음과 같은 조건을 만족해야 한다:

  • 클래스는 __iter__ 를 구현하되 자기 자신(self)을 반환해야 한다.
  • 클래스는 __next__ 메소드를 구현해서 Iterator를 next 내장 함수의 인자로 줬을 때 다음에 반환할 값을 정의해야 한다.
  • Iterator가 더 이상 반환할 값이 없는 경우는 __next__ 메소드에서 StopIteration 예외를 일으키도록 한다.

위의 세 가지의 요구사항을 모두 구현했을 때 그 클래스는 Iterator라고 할 수 있다. 실제 for 루프에 Iterable Object를 사용하면, 해당 Iterable의 __iter__()메서드를 호출하여 iterator를 가져온 후 그 iterator의 next() 메서드를 호출하여 루프를 돌게 된다. 

이때 __next__ 메소드를 주목할 필요가 있다. 앞서 Iterator는 끝에 다다를 때까지 원소를 하나씩 반환하는 특징이 있다고 했다. next 내장 함수는 인자가 되는 Iterator의 다음 인자를 반환하고 위치를 다음으로 옮기는 기능을 한다. for문은 Iterator를 강제적으로 현재 위치에서(첫 원소가 아닐 수 있다) 끝 원소까지 모두 반환하게 한다고 이해할 수 있다. 실제로 next 함수를 써보자.

iterator = iter([1, 2, 3, 4, 5])

print(next(iterator))
print(next(iterator))
print(next(iterator))


1
2
3

 list에 iter 함수를 써서 반환된 Iterator를 iterator 라는 변수에 할당했다. 이 Iterator는 내부에 __next__ 메소드가 구현되어 있기 때문에 next 내장 함수가 호출될 때마다 원소를 하나씩 반환한다. 위에서는 next를 세 번 호출했기 때문에 세 번째 값까지 호출됐다. 이 상태에서 iterator는 현재까지 값을 반환한 위치를 기억하고 있기 때문에 다음에 next를 호출하면 네 번째 값(4)이 반환되리라고 예상할 수 있다.

for n in iterator:
    print(n)

4
5

 이미 세 번의 next 함수 호출을 통해 3까지의 값이 반환되었기 때문에 남은 반환 횟수는 두 번이다. for 문을 통해서 네 번째 값부터 끝까지 출력할 수 있었다. iterator가 순회 상태를 관리하기 때문에 for문에서 다시 1부터 출력되는 것이 아닌 현재 상태값 4부터 출력하고 있다.

iterator는 일회용 깡통과 같아서 값을 모두 사용했다면 재사용할 수 없다. iterator는 마지막 원소까지 모두 반환했고 이후에 next를 통해 강제적으로 반환을 요구하면 내장 예외인 StopIteration이 반환된다.

 정리하면 Iterator는 Iterable에 iter 내장 함수를 적용해 반환되는 객체로서 next 함수를 통해 값을 한 번에 한 번씩 반환하는 특징이 있다. 내부적으로 현재까지의 반환 상태를 관리하고 조건에서 정의한 마지막까지 반환하면 더 사용할 수 없으며 StopIteration 예외를 일으킨다. 이게 Iterator의 주요 특징이다. 

 

4) 상속 관계

 Iterable과 Iterator는 collections.abc에 있는 다른 많은 자료구조처럼 다른 자료구조를 위한 추상 데이터 타입(Abstract Data Type, ADT)이다. ADT 사이에도 상속관계가 성립한다. 따라서 Iterable과 Iterator 사이에도 상속관계가 성립한다는 것이다. 결론적으로, Iterator는 Iterable이며, Iterator는 Iterable의 자식 클래스이고, Iterable을 상속받는다.


2. Generator의 개념

 

 Generator는 나만의 Iterable, Iterator 기능을 만들되, 생성 문법을 기존보다 단순화한 개념 또는 클래스라고 할 수 있다.리스트나 Set과 같은 컬렉션에 대한 iterator는 해당 컬렉션이 이미 모든 값을 가지고 있는 경우이나, Generator는 모든 데이타를 갖지 않은 상태에서 yield에 의해 하나씩만 데이타를 만들어 가져온다는 차이점이 있다. 이러한 Generator는 데이타가 무제한이어서 모든 데이타를 리턴할 수 없는 경우나, 데이타가 대량이어서 일부씩 처리하는 것이 필요한 경우, 혹은 모든 데이타를 미리 계산하면 속도가 느려서 그때 그때 On Demand로 처리하는 것이 좋은 경우 등에 종종 사용된다.

 Generator는 앞선 두 클래스와 마찬가지로 collections.abc에 저장되어 있다. Generator를 만드는 방법은 크게 두 가지이다.

 

1) 생성 방법 1 ; yield

 Generator 함수가 처음 호출되면, 그 함수 실행 중 처음으로 만나는 yield 에서 값을 리턴한다. Generator 함수가 다시 호출되면, 직전에 실행되었던 yield 문 다음부터 다음 yield 문을 만날 때까지 문장들을 실행하게 된다. 이러한 Generator 함수를 변수에 할당하면 그 변수는 generator 클래스 객체가 된다.

from random import randint

def random_number_generator(n):
    count = 0
    while count < n:
        yield randint(1, 100)
        count += 1

‘yield’ 문을 활용한 기초적인 Generator 활용 예제다. 사용해보기 전에, Iterator를 직접 구현했을 때와 대비되는 Generator의 특징을 살펴보면 다음과 같다.

  • 클래스가 아닌 함수로 정의한다.
  • 프로토콜처럼 Iterable와 Iterator의 두 요소를 분리하지 않고 한 요소에 담을 수 있다.
  • 호출될 때마다 한 번씩 반환할 값을 반환하는 키워드가 ‘yield’이며, 이는 함수가 아니다. 즉 return 문처럼 ()를 사용하지 않는다.

 이제 이를 직접 만들어서 활용해보자. 랜덤 정수를 5개 반환하는 Generator를 만들어보자.

g = random_number_generator(5)

print(g)

<generator object random_number_generator at 0x7f0801e15e08>

 함수를 호출해 generator 객체(object)를 만들었다. 이때 객체라는 것은 함수의 반환값이 상태값을 유지하는 Generator 클래스의 인스턴스(instance)라는 것을 암시한다. Generator는 Iterable, Iterator 생성 문법을 간략화한 개념 및 구현이기 때문에 그 내부과정이 직관적으로 보이지는 않지만(추상화되었기 때문에), 함수의 호출 결과가 결국은 Iterator protocol을 준수하는 개체를 반환한다고 이해하면 무난하다.

 생성된 Generator를 사용해보자.

print(next(g))
print(next(g))
print(next(g))

72
90
3

 예상했던 대로 next 함수의 인자에 넣어서 하나의 값씩 반환할 수 있다. 총 5개의 값을 반환할텐데 이미 3개를 반환했으므로 남은 반환 횟수는 2번이 된다.

print(next(g))
print(next(g))
print(next(g))

48
34
----> 3 print(next(g))

StopIteration: 

# 총 5번의 호출 이후 StopIteration이 반환됐다.

 예상했겠지만 다른 generator object를 만들어서 for문에 넣어도 문제없이 동작한다.

 

2) 생성방법 2 ; Generator comprehension

 ()를 사용해서 comprehension을 만들면 tuple이 생성되지 않고 다음과 같은 결과가 나온다.

print((n for n in range(3)))

<generator object <genexpr> at 0x7f0801c6f620>

 []를 사용해 만든 comprehension은 list를 만들었다. 그러면 ()를 사용하면 tuple이 반환되리라 무난하게 예상할 수 있다. 그런데 예상과는 정반대로 ()를 사용하면 tuple이 아닌 ‘generator object’가 생성됐다. 즉, list comprehension의 문법을 사용하되 식을 닫는 괄호를 ()를 사용하는 방법이 generator를 생성하는 두 번째 방법이며, 이를 Generator comprehension(또는 generator expression)이라고 한다. 하지만 Generator Expression은 List Comprehension과 달리 실제 리스트 컬렉션 데이타 전체를 리턴하지 않고, 그 표현식만을 갖는 Generator 객체만 리턴한다. 즉 실제 실행은 하지 않고, 표현식만 가지며 위의 yield 방식으로 Lazy Operation을 수행하는 것이다.

 위에서 사용한 예제를  generator comprehension을 사용해서 다시 만들어보자. 

from random import randint

g = (randint(1, 100) for _ in range(5))

 list comprhension을 통해 list를 손쉽고 짧게 만들 수 있었던 것처럼 generator expression을 통해 Generator를 쉽게 만들 수 있었다. 사용해보면 결과는 다음과 같다.

for n in g:
    print(n)

96
34
71
13
33

 

 그러면 다음과 같은 질문을 던질 수 있다. Iterable, Iterator, Generator에서 Generator만 쓰면 되는가? 또 Generator를 사용한다고 할 때, yield를 사용하는 대신 Generator expression을 사용하는 방법이 무조건 옳은 것인가? 보다 복잡한 프로그램일수록 Iterator protocol을 준수한 Iterable, Iterator를 만들어 쓰는 것이 좋은 선택일 수 있고, 프로그램이 매우 단순하다면 Generator expression을 사용하는 방법이 더 바람직할 수 있다. 

 

3) 상속관계

 Generator는 Iterator의 자식 클래스다. 하지만 그 역은 성립하지 않는다. 세 ADT의 상속관계가 아래와 같이 일렬로 정렬됨을 확인할 수 있다.

Iterator는 Iterable의 자식 클래스다. Generator는 Iterator의 자식 클래스다. 따라서, Generator는 Iterable의 자식 클래스다.

https://qiita.com/keitakurita/items/5a31b902db6adfa45a70

https://shoark7.github.io/programming/python/iterable-iterator-generator-in-python

https://mingrammer.com/translation-iterators-vs-generators/#%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0-generator

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

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

 

728x90