[Python] Iterator & Generator

송진수·2021년 7월 9일
0

Iterator

값을 순차적으로 불러올 수 있는 객체.
Iterator를 만들 수 있는 객체를 iterable이라고 부른다.

파이썬에서 자주 사용하는 리스트, 튜플, 집합, 딕셔너리같이(range, enumerate, map, zip같은 함수로 만들어지는 객체들도) for문에 사용할 수 있는 객체는 모두 iterable하다고 보면 된다.

Iterator 만들기

dir 함수를 통해 호출한 객체의 attribute중에서 __iter__ 이 있으면 iterable이다.

iter함수는 객체의 __iter__ 속성을 리턴하는 함수이다.

a = [1,2,3]
b = iter(a)
print(b) 	# <list_iterator object at 0x1086c70d0>

이렇게 만들어진 iterator 객체 b는 __next__ 속성을 통해 값을 순차적으로 불러올 수 있으며, next 함수는 iter 함수와 마찬가지로 객체의 __next__ 속성을 리턴한다.

print(next(b))	# 1
print(next(b))	# 2
print(next(b))	# 3
print(next(b))

# Traceback (most recent call last):
#   File "<pyshell#69>", line 1, in <module>
#     print(next(b))
# StopIteration

iterator가 불러올 수 있는 값들을 모두 불러오고 다시 불러오면 StopIteration이 발생한다.

따라서 StopIteration이 발생할 때 함수 실행을 종료한다면, for문을 직접 구현할 수 있다.

a = [1,2,3]
iterator_a = a.__iter__()
I = iter(a)
while True:
    try:
        n = next(I)
    except StopIteration:
        break
    print(n**2, end=' ') 	   # 1 4 9

Generator

Iterator가 컬렉션의 모든 값을 가지고 있는 것과 달리, generator는 yield에 의해 값을 하나씩 호출하는 특성을 가지고 있다. 따라서 generator를 생성하려면 yield 문이 필요하다.

def one_generator():
    yield 1
    yield 2
    return 'return에 지정한 값'
    
try:
    g = one_generator()
    print(type(g))		# <class 'generator'>
    print(next(g))		# 1
    print(next(g))		# 2
    print(next(g))		# 'return에 지정한 값'
except StopIteration as e:
    print(e) 

이렇게 만들어진 객체 g는 generator가 된다. generator를 next함수로 한번씩 호출할 때마다, 만나는 yield에서 값을 반환하고, 다시 호출하면 그 yield 부터 다음 yield까지의 코드를 실행한다.

generator 객체는 send를 통해 yield 단계에서 대기하고 있는 generator에 원하는 값(혹은 수식)을 전달하는 것도 가능하다!

def generator_send():
    received_value = 0

    while True:

        received_value = yield
        print("received_value = ",end=""), print(received_value)
        yield received_value * 2

gen = generator_send()	# generator 객체 생성
next(gen)		# generator_send 함수의 첫번째 yield까지 실행
print(gen.send(3))	# received value = 3
			# 6	
            		# send를 통해 3을 전달하여 received_value = 3 으로 다음 yield까지 실행 후, 
                        # 반환값인 6을 print

next(gen)		# while문을 돌아 다시 첫번째 yield까지 실행
print(gen.send(2))	# 위 과정을 반복

Generator Comprehension

list comprehension과 마찬가지로 generator도 comprehension이 가능한데, [] 대괄호가 아닌 () 소괄호를 이용한다.

def generate_square_from_list():
    result = ( x*x for x in L )
    print(result)
    return result

def print_iter(iter):
     for element in iter:
         print(element)

print_iter( generate_square_from_list() )

# 1
# 4
# 9

List comprehension과의 차이점

List comprehension이 모든 값을 가진 컬렉션(리스트)를 생성하는 것과 달리, generator comprehension은 표현식만을 갖는 generator 객체를 생성한다.

따라서 이렇게 만들어진 generator는 호출할 때마다 위 yield의 기능처럼 하나씩 값을 반환하고 그 상태를 유지한다.

import time

L = [1,2,3]

def print_iter(iter):
    for element in iter:
        print(element)

def lazy_return(num):
    print("sleep 1s")
    time.sleep(1)
    return num

print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)

print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)


# comprehension_list=
# sleep 1s
# sleep 1s
# sleep 1s
# 1
# 2
# 3
# generator_exp=
# sleep 1s
# 1
# sleep 1s
# 2
# sleep 1s
# 3

위 실행결과처럼 list comprehension은 리스트의 모든 요소를 생성하기 때문에 리스트 요소 출력을 시작하기 까지 3초가 소요되는 반면, generator comprehension은 그 표현식에 따라 값을 하나씩 반환하기 때문에 1초에 하나씩 값을 출력한다.

Generator Comprehension의 장점은 데이터가 너무 커서 모든 데이터를 리턴할 수 없는 경우, 연산 시간이 너무 오래 걸려서 일부만 처리하는 것이 필요한 경우 등에 사용할 때 유용하다는 것이다.

profile
보초

0개의 댓글