Programming/Python

Python 이터레이터(iterator)와 제네레이터(generator)

daeunnniii 2022. 1. 12. 12:48
728x90
반응형

이터러블(Iterable) 객체

for문이나 while문과 같은 반복문에서 사용할 수 있는 객체를 우리는 이터러블(iterable)하다고 한다.

파이선 내에서 제공하는 iter() 내장 함수로 객체가 이터러블(iterable)한지 알 수 있다.

a=1 에서 1은 iterable하지 않으므로 반복문에서 사용할 수 없다.

>>> a = 1
>>> print(iter(a))
Traceback (most recent call last):
  File "/Users/brayden/PycharmProjects/study/run.py", line 2, in <module>
    print(iter(a))
TypeError: 'int' object is not iterable
>>> for i in a:
    	print(i)
Traceback (most recent call last):
  File "/Users/brayden/PycharmProjects/study/levelup/03.py", line 3, in <module>
    for i in a:
TypeError: 'int' object is not iterable

리스트 타입이나 튜플, 딕셔너리 타입의 경우 iterable 객체에 해당하므로 반복문에서 사용할 수 있다.

>>> a = [1, 2, 3]
>>> print(iter(a))
<list_iterator object at 0x7fbf1567c810>
>>> for i in a:
    	print(i)
1
2
3

 

Iterator 객체와 __next__ 메서드

어떤 객체가 __iter__ 메소드를 포함하고 있다면 이 객체를 이터러블(iterable) 객체라고 한다.

즉, 리스트, 튜플, 딕셔너리와 같은 자료구조에는 내부적으로 __iter__ 메서드가 구현되어있으며 해당 메서드에서 iterator 객체를 리턴한다. 우리는 이 리턴된 iterator 객체를 통해 반복문을 수행하게 된다.

 

iter() 내장 함수를 호출하면 내부적으로 해당 객체의 __iter__ 메서드를 호출하게 된다.

__iter__ 메서드는 iterator 객체를 반환해준다. 여기서 __iter__ 메서드는 반드시 __next__ 메서드를 구현하고 있어야한다.

 

따라서 반복 가능한 성질을 갖도록 객체를 만들고자할 때 iterator를 사용할 수 있다. 클래스 내에서 __next__와 __iter__ 메서드를 구현해주면 된다.

 

예를 들어 Season 클래스에서 봄, 여름, 가을, 겨울을 각각 출력하는 iterable한 객체를 만들려고 한다.

 

class Season:
    def __init__(self):
        self.data = ["봄", "여름", "가을", "겨울"]
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.data):
            cur_season = self.data[self.index]
            self.index += 1
            return cur_season
        else:
            raise StopIteration

__iter__ 메서드는 iterator 객체를 리턴해야하는데 Season 클래스는 자기 자신이 __next__ 메서드를 포함하는 iterator 객체이므로 return self로 구현하면 된다.

 

__next__ 메서드는 self.index 값이 self.data의 크기를 넘지 않으면 현재 계절을 리턴해주고, self.index 값을 1씩 증가시킨다. 그리고 self.index 값이 self.data의 크기인 4가 되거나 4보다 크다면 StopIteration 예외를 발생시킨다.

 

이제 Season 클래스를 통해 s 객체를 생성하고, iter()를 호출하여 iterator 객체를 얻는다. 그리고 next() 내장함수를 호출하여 값을 얻어오면 계절 이름이 순서대로 출력된다.

s = Season()
ir = iter(s)
print(next(ir))
print(next(ir))
print(next(ir))
print(next(ir))

 

현재 iterator 객체를 구현한 것이므로 다음과 같이 반복문에서도 사용할 수 있다.

 

s = Season()      # 클래스 객체 생성
for i in s:
    print(i)

 

 

제네레이터(Generator)

제네레이터 함수는 Iterator를 생성해주는 함수이다. 제네레이터(Generator)는 쉽게 말하면 return 키워드 대신 yield 키워드를 사용하는 함수이다. 'yield'가 하나라도 있으면 제네레이터가 된다.

  • return 키워드: 결과값을 돌려주고 함수가 끝나버림.
  • yield 키워드: 결과값을 돌려주지만 실행의 상태를 그대로 저장해 둔 채 잠시 멈추었다가, 나중에 멈춘 상태부터 다시 이어서 다음 코드를 실행함.

 

def generator():
    for i in range(5):
        yield i

g = generator()       # 제너레이터 객체 생성 

n1 = next(g)
n2 = next(g)
n3 = next(g)
n4 = next(g)
n5 = next(g)

print(n1, n2, n3, n4, n5)

next(g)				# StopIteration 예외 발생

 

yield가 사용된 함수는 함수 객체를 생성해도 코드가 바로 실행되지 않는다.

함수 객체를 생성하면 제네레이터(generator) 객체가 생성되고 실행 준비를 한다.

그리고 next() 함수 호출을 통해 제네레이터 객체 내의 코드를 실행할 수 있다. 이때 yield 구문을 만나면 yield 키워드에 있는 값을 리턴하고, 실행 흐름도 호출부로 이동한다.

다만 일반 함수들과 다르게 코드가 중지된 시점의 상태를 유지할 수 있어 해당 상태로부터 다시 코드를 이어서 실행할 수 있다.

 

next() 메서드를 다시 호출하면 제네레이터가 중지된 상태에서부터 코드가 실행된다.

for문에서 i = 1 까지 실행했으므로 이번에는 i가 2를 바인딩한 후 yield 2 구문가지 코드가 실행된다.

마찬가지로 yield 키워드를 만나면 2를 호출부로 리턴하고 실행 흐름도 호춟로 이동한다.

 

next(g)를 5번 넘게 호출하면 더이상 돌려줄 값이 없기 때문에 StopIteration 에러가 발생한다.

 

for문과 Generator

다음과 같이 for문을 활용하면 next() 메서드를 호출하지 않아도 제네레이터로부터 값을 가져올 수 있다.

for문을 사용하면 반복할 때마다 StopIteratoin 예외가 발생하기 전까지 내부적으로 next(g)를 호출한다.

 

def generator():
    for i in range(5):
        yield i

g = generator()       # 제너레이터 객체 생성 

for i in g:
	print(i)

 

Send로 generator 함수에 값 전달하기

gen_send() 함수yield로 send를 통해 받은 값을 received_value에 할당하고 그 값의 2배를 리턴받는다.

generator에서는 이처럼 yield를 이용해 generator 함수 실행 중 값을 전달할 수 있고, 응용하면서 generator 함수를 사용해 main 문에서 연산 결과에 따라 호출도 제어할 수 있다.

def gen_send():
    received_value=0
    
    while True:
        received_value = yield
        print("received_value= ", end=""), print(received_value)
        yield received_value * 2
        
gen = gen_send()
next(gen)
print(gen.send(2))

next(gen)
print(gen.send(3))

 

결과:

received_value = 2
4
received_value = 3
6

 


Generator Expression

generator 표현식은 list comprehension과 비슷하지만 대괄호[ ] 가 아닌 괄호( )를 사용하여 작성한다.

L = [1,2,3]

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())

결과

<generator object generate_square_from_list.<locals>.<genexpr> at 0x7f88e80de430>
1
4
9

 

 

Generator Expression과 List comprehension 비교

다음은 Generator Expression과 List comprehension을 비교하는 코드이다.

import time
def print_iter(iter):
    for e in iter:
        print(e)

def sleep_return(n):
    print("sleep 1 second")
    time.sleep(1)
    return n

print("===== list comprehension =====")
lst_comprehension = [sleep_return(i) for i in range(3)]			# list comprehension
print_iter(lst_comprehension)

print("===== generator expression =====")
generator_exp = (sleep_return(i) for i in range(3))				# generator expression
print_iter(generator_exp)

 

실행 결과는 아래와 같다.

list comprehension는 대괄호 [] 내의 sleep_return() 함수를 다 실행 시킨 뒤에 그 다음 행의 print_iter() 함수를 호출하므로 "sleep 1 second"를 3번 출력한 뒤에 0~2가 출력된다.

 

반면에 generator expression은 실제로 값을 출력하기 전까지는 소괄호 () 내의 generator expression가 실행되지 않았다. 즉, 실제로 generator_exp의 값을 사용하는 순간에만 함수를 실행한다.

 

print_iter(generator_exp)에서 generator_exp의 값을 사용해야하므로 그 순간에만 sleep_return()을 호출한 뒤 0이라는 값을 리턴받아 print_iter() 함수를 실행한다. 1, 2도 마찬가지이다.

 

값이 실제로 사용되지 않으면 연산 또한 하지 않으므로 시간과 메모리를 절약할 수 있다.

 

===== list comprehension =====
sleep 1 second
sleep 1 second
sleep 1 second
0
1
2

===== generator expression =====
sleep 1 second
0
sleep 1 second
1
sleep 1 second
2

 

 

 

 


Generator의 장점

프로그램을 개발할 때 처리해야하는 숫자가 커지며, 사용되는 메모리가 증가하는 상황이 발생한다.

이대 작은 데이터에서는 잘 동작하는 프로그램이 큰 규모의 데이터셋에서는 제대로 동작하지 않거나, 매우 느리게 동작하게 된다.

제네레이터(Generator)를 사용하면 굳이 데이터를 한번에 모아서 처리하지 않기 때문에 큰 메모리를 사용하지 않아도 된다. 따라서 규모가 있는 프로그램을 개발할 때 매우 중요한 요소라고 볼 수 있다. 제네레이터는 큰 규모의 확장성이 있는 프로그램을 개발하기 위해 사용된다. 즉, 제네레이터의 장점은 메모리 공간의 효율성이라고 볼 수 있다.

 

 

빵 만들기 예시

1) Generator를 사용하지 않은 빵 만들기 예

빵 만들기(100)을 호출하면 빵 100개를 담은 리스트를 모두 메모리에 로드해야한다. 하지만, 빵 포장은 빵을 하나씩 가져와서 포장하므로 미리 100개의 빵을 메모리에 로드해둘 필요가 없다.

def 빵만들기(n):
    빵쟁반 = []
    for i in range(n):
        빵 = "빵" + str(i)        # 빵0, 빵1, ..., 빵99
        빵쟁반.append(빵)
    return 빵쟁반

def 빵포장(빵):
    print("{} 포장완료".format(빵))

for i in 빵만들기(100):
    빵포장(i)

 

2) Generator를 사용한 빵 만들기

yield 키워드를 활용하여 더 효율적으로 빵 만들기를 진행할 수 있다.

def 빵만들기(n):
    for i in range(n):
        빵 = "빵" + str(i)        # 빵0, 빵1, ..., 빵99
        yield 빵

def 빵포장(빵):
    print("{} 포장완료".format(빵))

for i in 빵만들기(100):
    빵포장(i)

 

 

 

 

 

 

 

참고: https://wikidocs.net/74398

https://velog.io/@soojung61/Python-generator

 

 

 

 

 

 

728x90
반응형