[Python] 이터레이터와 제너레이터

이터레이터와 제너레이터는 여러개의 요소를 가지는 컨테이너를 다루는 간편한 방법을 제공하는 객체이다.

  • 아래 사진은 이터레이터와 제너레이터 뿐만 아니라 컨테이너 등 다른 개념 간의 상관관계에 대해 이해하기 쉽게 그림으로 표현한 것이다.

들어가기 전에..

이터레이터와 제너레이터의 개념을 설명하기 전에 이해하는데 도움이 될 만한 개념들에 대해 정리해 보았다.

1. 컨테이너(Container)

말 그대로 무언가를 담고 있는 것, 하나 이상의 숫자나 문자열 등 파이썬의 모든 값들을 가지는 묶음을 의미한다.
컨테이너에는 리스트, 튜플, 세트, 딕셔너리, 문자열 등이 있다.

멤버쉽 테스트를 통해 어떤 객체가 특정 원소를 포함하고 있는지 아닌지를 판단할 수 있으면 컨테이너라고 부른다.

2. 멤버쉽 테스트

if a in b와 같이 어떤 객체에 특정한 원소를 가지고 있는지를 테스트하는 연산자를 멤버쉽 연산자라고 한다.

  • 가정 설정문(assert)
    if a in b와 같은 멤버쉽 연산자도 존재하고, 예외 처리를 사용하는 방법도 존재하는데 왜 가정 설정문을 사용해야 할까?
    방어적 프로그래밍을 수행할 수 있다.
list = [1, 2, 3, 4, 5, 5.5, 6, 7.5, 8]

def examples(a):
	assert type(a) is int, 'int형이 아닌 값이 존재한다.'
    
for i in list:
	test(i)

>>>
AssertionError: int형이 아닌 값이 존재한다.

위 코드와 같이 "assert 조건, '메시지'" 형식으로 작동하며, 메시지는 생략할 수 있다.
단순한 에러를 찾는것이 아니라 위와 같이 값을 보증하기 위해 프로그램 만드는 과정에 참여해 원하는 조건에 맞는 변수 값을 보증받을 때까지 assert로 테스트한다.
이런 실수를 가정해 값을 보증하는 방식으로 코딩을 하는것을 방어적 프로그래밍이라 부른다.

3. 컴프리헨션(Comprehension)

Comprehension은 iterable한 객체를 생성하기 위한 방법 중 하나이다.
파이썬에는 크게 네가지 종류의 Comprehension이 있다.

→ List Comprehension(LC), Set Comprehension(SC), Dict Comprehension(DC), Generator Expression(GE)

이 중 대표적인 컴프리헨션인 List Comprehension에 대해서만 설명하고, Generator Expression은 이후 설명할 제너레이터 내용에 포함시킬 것이다.

  • List Comprehension(LC)
    컴프리헨션 중 가장 대표적으로 사용되는 리스트를 쉽게 생성하기 위한 방법
# 20까지의 홀수 출력하기
odds = [x*2+1 for x in range(10)]
>>> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
# a^2 + b^2 = c^2 (a<b<c)를 만족하는 피타고라스 방정식의 해를 찾는 LC
solution = [(x, y, z) for x in range(1, 30) for y in range(x, 30) for z in range(y, 30) if x**2 + y**2 == z**2
>>>
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (7, 24, 25), (8, 15, 17) ...]

Iterator? Iterable? Generator?

1. Iterable

앞서 설명한 대부분의 컨테이너는 Iterable하다. 그러나 Iterable이라는 특성은 데이터 구조에 국한되는게 아닌 이터레이터를 반환할 수 있는 모든 객체가 가질수 있는 특성이다.

x = [1, 2, 3, 4, 5]
y = iter(x)
z = iter(x)
next(y)
next(y)
next(z)
type(x)
type(y)

>>>
1
2
1
<class 'list'>
<class 'list_iterator'>

위 코드에서 살펴볼 수 있듯이 컨테이너인 리스트는 Iterable한 특성을 가진다. 하지만 x를 이터레이터라고 볼 수는 없다. y와 z같이 Iterable한 x로 부터 이터레이터 인스턴스를 통해 값을 생성해낸 것이 이터레이터이다.

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

또한 위와 같은 코드에서는 내부적으로 다음 연산이 수행된다.

이터레이터(Iterator)

  • next()를 호출할 때 다음 값을 생성해내는 상태를 가진 헬퍼 객체이다. 즉, next()를 가진 모든 객체는 이터레이터이다.
  • next()를 통해 다음 값을 계산해내는 값 생성기라고 할 수 있다.

이터레이터의 특성

  • __next__ 메서드를 사용해 모든 요소를 출력한 이후의 __next__메서드는 StopIteration 에러를 발생시킨다.
#이터레이터 확인을 위한 리스트
list = [1, 2, 3]

#__iter__() 메서드를 사용해 이터레이터 형태로 변환
test_iter = iter(test) #또는 test_iter = test.__iter__()

#__next__() 메서드를 사용해 요소를 하나씩 불러옴
print(test_iter.__next__())
print(test_iter.__next__())
print(test_iter.__next__())
print(test_iter.__next__())

>>>
1
2
3
StopIteration 에러 발생
  • itertools 함수를 통해 이터레이터를 생성할 수 있으며 무한정 동작하는 시퀀스를 만들거나, 개수가 정해진 시퀀스로부터 무한정 값을 출력받는 등 다양한 동작을 수행할 수 있다.
# 피보나치 수열을 생성하는 이터레이터
class fib:
	def __init__(self):
    	self.prev = 0
        self.curr = 1
        
    def __iter__(self):
    	return self
        
    def __next__(self):
    	value = self.curr
        self.curr += self.prev
        self.prev = value
        return value
f = fib()
list(islice(f, 0, 10))

>>>
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
  • next()를 호출할 때 마다 두 가지 중요한 작업이 수행된다.
  1. 다음 next() 호출을 위해 상태를 변경한다.
  2. 현재 호출에 대한 결괏값을 생성한다.

Lazy Evaluation

이터레이터는 값을 요청할 때 까지 동작을 하지 않다가 요청 받을 때만 동작을 수행한다. 하나의 값을 생성한 후에는 다시 유휴상태가 된다.

제너레이터(Generator)

  • iter()와 next()를 사용하는 이터레이터를 만드는 대신, 간결한 문법으로 iterable한 동작을 수행한다.
  • 모든 제너레이터는 이터레이터이다. 하지만 그 반대는 성립하지 않는다.

제너레이터의 특성

  • 모든 제너레이터는 값을 그 때 그 때 생성하는 Lazy Evaluation 특성을 가지고 있다.
  • 함수에서 return 대신에 yield를 사용해 값을 반환한다.
#제너레이터로 작성된 피보나치
def fib():
	prev, curr = 0, 1
    while True:
    	yield curr
        prev, curr = curr, prev + curr
        
f = fib()
list(islice(f, 0, 10))

>>>
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

제너레이터의 타입

파이썬에서 제너레이터는 제너레이터 함수와 제너레이터 표현식으로 나뉜다.

  • 제너레이터 함수
    위의 제너레이터로 작성된 피보나치와 같은 yield 키워드가 포함된 함수들을 의미한다.

  • 제너레이터 표현식(Generator Expression)
    글의 초반부 컴프리헨션에 대해 정리할때 설명한 제너레이터 표현식은 리스트 컴프리헨션이나 set, dict에 대한 컴프리헨션과 동일한 제너레이터이다.

#List Comprehension
numbers = [1, 2, 3, 4, 5, 6]
a = [x * x for x in numbers]
print(a)

>>>
[1, 4, 9, 16, 25, 36]

리스트 컴프리헨션을 표현하기 위해 대괄호 []를 사용했듯이, 세트나 딕셔너리에도 중괄호 {}를 사용한다.

이 때, 제너레이터 표현식에는 소괄호 ()를 사용한다.

#Generator Expression
lazy_squares = (x * x for x in numbers)
lazy_squares
>>>
<generator object <genexpr> at 0x10d1f5510>

# 이터레이터와 동일하게 next()를 사용해 다음 값을 반환받는다.
# 이터레이터의 Lazy Evaluation 특성대로 값을 반환한 후 유휴상태에 돌입한다.
next(lazy_squares)
>>> 
1

#list()나 set()등의 메소드를 사용하면 다시 iterable 객체로 바꿀 수 있다.
list(lazy_squares)
>>>
[4, 9, 16, 25, 36]

좋은 웹페이지 즐겨찾기