iterator?

  • 값을 순차적으로 꺼낼 수 있는 객체; value factory
  • List, Dict, Set, Tuple, Str 등 순회 가능한 sequential 객체 = iterable object
  • dir로 객체를 호출하여 __iter__함수가 있으면 이터레이터
  • __next__() 메소드를 가진 모든 객체는 이터레이터 → 다음요소를 하나씩 꺼내오는 함수
L = [1, 2, 3]
iterator_L = iter(L)
print(iterator_L.__next__())  #1 
print(iterator_L.__next__())  #2
print(iterator_L.__next__())  #3
print(iterator_L.__next__())  #StopIteration 에러

 

 

 

Iterable vs Iterator

:이터러블은 이터레이터를 반환할 수 있는 모든 객체이다. (꼭 자료구조여야만 하는 것은 아니다.)

이터러블한 객체를 iter()을 사용하여 이터레이터 객체를 반환한다.

예시

x = [1, 2, 3]   #iterable 
y = iter(x)     #instance of an iterator 
print(next(y))
print(next(y))

print(type(x))
print(type(y))

결과:
1
2
<class 'list'>
<class 'list_iterator'>

 

 

 

 

iter & next

  • iter는 객체의 __iter__ 함수를 호출하는 함수
  • next는 객체의 __next__ 함수를 호출하는 함수

 

L = [1, 2, 3]

I = iter(L)
while True:
        try:
                x = next(I)
        except StopIteration:
                break
        print(x**2, end=" ")

 

 

이터레이터는 size 확인하기가 불가능해서 데이터 길이 만큼만 while문을 실행하지 못했다.

D = {'a':1, 'b':2, 'c':3}
d_iterator = iter(D)
while True:
    d_next = next(d_iterator)
    print(d_next)

위의 예시처럼 StopIteration이 발생하는데

이 오류가 발생하지 않도록 하는 방법이 뭘까???

 

 

예외처리문으로 StopIteration 이 발생하는 경우 무한반복을 탈출(break) 해주면 된다.

d_iterator = iter(D)
while True:
        try:
                d_next = next(d_iterator)
                print(d_next)
        except StopIteration: 
                break

 

 

 

 

 

Generator?

  • 이터레이터를 생성하는 함수

  • 모든 값을 포함하여 반환하는 대신 호출할 때마다 하나의 값을 리턴

  • 따라서 작은 메모리로 효율적으로 대용량의 이터러블을 순회할 수 있다는 것이 큰 장점

  • dir로 확인해보면 __iter__, __next__ 함수다 모두 들어있음 따라서 __iter__를 호출할 필요 없이 바로 __next__함수가 모두 들어있음

  • yield : 제너레이터 함수에서 값을 반환할 때 사용되고 yield가 호출된 후 다음 next가 호출되기 전까지 hold하다가 next가 호출되면 이전 상태에 이어서 다음 연산 수행

    여기서 yield 값은 어떤 메소드를 사용했냐에 따라 다르다.

    • __next__() : None
    • send() : 메소드로 전달된 값

 

 

 

예제1

def generator_send():
    received_value = 0
    while True:
        received_value = yield
        print('received_value: ', received_value)
        yield received_value**2 

gen = generator_send()
print(gen)  
# <generator object generator_send at 0x000001DEA95A63C0>

next(gen)
print(gen.send(2))
#received_value:  2
#4

next(gen)
print(gen.send(3))
#received_value:  3
#9

 

동작

  1. yield문이 포함된 제너레이터 함수를 실행하면 제너레이터 객체가 반환되는데 이 때는 함수의 내용이 실행되지 않는다.(그저 변수에 함수를 담을뿐!) → gen = generator_send() 구문 이야기 이다.
  2. next()라는 빌트인 메소드를 통해 제너레이터 함수를 실행시킬 수 있으며 next()메소드 내부적으로 iterator를 인자로 받아 이터레이터의 __next__() 메소드를 실행시킨다. → next(gen) 구문
  3. 처음 __next__() 메소드를 호출하면 함수의 내용을 실행하다 yield문을 만났을 때 처리를 중단한다.
  4. 이 때 모든 local state는 유지되는데 변수의 상태, 명령어 포인터, 내부 스택, 예외 처리 상태를 포함한다.
  5. 그 후 제어권을 상위 컨텍스트로 양보(yield)하고 또 __next__()가 호출되면 제너레이터는 중단된 시점부터 다시 시작한다.

 

 

 

그러니까 gen.send(2)를 실행하면 함수내부에서 received_value에는 2가 들어가고 2**2인 4를 반환한다.

이상태 에서 다시 호출될때까지 제너레이터는 대기상태!

 

 

 

예제2

def gen():
    print('gen start')
    data1 = yield 1 
    print(f'gen[1]: {data1}')
    data2 = yield 2
    print(f'gen[2]: {data2}')
    return 'done'
g = gen()

#data1 input -> 1st print, 1 출력 
first = g.send(None)
print(first)

#data2 input  -> 2nd print, 2 출력 ㅣㅔ
second = g.send('world')
print(second)

#data3 input -> 3rd print, StopItration: done 출력 (return 'done 을 yield 3 으로 바꾸면 3 출력 )
print(g.send('hello'))

 

 

 

 

 

 

Generator expression

  • 리스트 컴프리핸션과 동일한 방식의 표현식
  • 소괄호를 사용하지만 tuple comprehension 이 아님!

💡예시

numbers = [1, 2, 3, 4, 5, 6]

lazy_squares = (x * x for x in numbers)

>>> lazy_squares
<generator object <genexpr> at 0x10d1f5510>

>>> print(next(lazy_squares))
1
#❗️아래에서 list가 4부터 시작하는 이유는 위에서 next()메소드로 맨 처음 값을 이미 출력했기 때문❗️

>>> print(list(lazy_squares))
[4, 9, 16, 25, 36]

→ def에 yield를 사용해서 또 Generator expression를 사용해서 generator객체를 만들어봤다.

 

 

 

Generator의 효율성

사실 결과만이 목적이면 이런 어려운 개념을 공부할 필요없이 일반 list comprehesion를 쓰거나, 함수를 만들어쓰면 같은 결과를 구할 수 있다.

우리의 랜덤 정수 예제도 그냥 list로 반환해서 써도 어지간한 요구사항은 똑같이 만족시킬 것이다.

그럼에도 불구하고 어떤 추가적인 장점이 있기에 generator나 iterator을 쓰면 좋은 것일까???

밑에서 알아보겠다!

 

- Lazy evaluation을 통한 메모리 안전성

이 구절의 정확한 의미를 이해하기 위해 for문에서 같은 결과를 내는 list comprehension과 generator를 비교해서 살펴볼 것이다.

 

for문을 통해 1부터 10까지 출력하고 싶다고하자.

이때 in 키워드 뒤에 다음과 같은 list와 generator을 각각 둬서 같은 결과를 낼 수 있다.

for e in [n for n in range(1,11)]:
		print(e)

>>>
1
2
3
...

 

for e in (n for n in range(1, 11)):
		print(e)

>>>
1
2
3
...

위의 두 코드는 정확히 같은 결과를 출력하면서 식도 비슷하다.

차이라면 식을 완성하기 위해 [], ()의 사용유무 차이이다.

첫번째 에서는 list가 반환돼어 쓰였고, 두번째에서는 Generator가 반환돼 쓰였다.

하지만 이 둘에는 큰 차이가 있는데 내부적으로 컴퓨터 메모리와 관련이 있다.

 

 

먼저 리스트를 만들었던 첫 번째 예를 살펴보자. 리스트를 선언하면 하드웨어적으로 메모리에 그 리스트를 위한 공간이 할당된다.

그것을 그림으로 표현하면 다음과 같다.

 

 

C 같은 언어에서는 일반적으로 정수 배열이 메모리 상에 연속적으로, 단절 없이 할당된다.

그리고 여기서 쓰인 정수는 int형으로 4바이트를 차지한다.

 

위 그림에서 각 셀이 배열(array)의 한 원소를 나타내고 셀의 바로 밑에 있는 숫자는 각 원소가 차지하는 메모리의 위치 주소를 의미한다. 또한 주소 숫자 간격이 4라는 것을 알 수 있다.

파이썬에서 list가 c에서의 array와는 구현이 조금 다르지만

중요한 것은 1번과 같이 list comprehension 식을 입력했을 때 메모리에 배열의 크기에 비례하는 공간이 바로 할당된다는 것이다.

위의 예에서라면 10개의 정수가 배열에 있으므로 이 배열의 총 크기는 40 byte(4 * 10)이 될 것이다.

 

 

하지만 두 번째 에서는 다르다. generator expression을 통해 생성한 generator는 숫자 10개를 생성할 예정이지만 그것을 배열 등의 구체적인 형태로 가지고 있지 않다.

다시 말해

generator expression은 지정한 규칙대로 값을 반환할 규칙과 현재 어디까지 반환했는지 들을 관리할 여러 상태 값을 담고 있지만 배열과 달리 값 모두를 generator를 생성할 당시에 메모리에 할당하지 않는다는 결정적인 차이가 있다.

 

이게 첫 번째 예제로 만든 list와의 큰 차이로서 이런 형태의 디자인 패턴을 lazy loading이라고 한다.

이 lazy loading라는 패턴은 생각 보다 흔히 쓰이기 때문에 기억하고 있으면 좋다.

반대로 첫 번째 예제처럼 생성과 동시에 메모리에 적재하는 패턴은 eager loading라고도 한다.

 

다음과 같은 질문을 던질 수 있다. 왜 굳이 이 둘을 분리해서 쓰는가? 결국 generator(iterator)을 통해 할 수 있는 모든 일은 list comprehension으로 잘할 수 있는데 말이다.

 

이제 존재의 이유를 보여드리겠습니다.

만약 list의 크기가 10이 아니라 아주아주 큰값이라면 어떨까요?

big_list = [i for i in range(1, 100000000000000000000+1)]

# 생성이 안 됨

 

아까와 달리 크기가 매우 큰 list를 생성하는 list comprehension이다.

range함수 내의 두 번째 인자가 정확히 몇인지 눈으로 셀수는 없을 정도다.

저 한 줄을 복사해서 파이썬 인터프리터에 실행시켜보라. 결과가 어떻게 나올까??

 

 

아마 멈추거나 인터프리터가 종료되거나 할 것이다.

그 이유는 list와 같은 자료구조는 eager loading으로 원소의 크기에 따라 총 크기가 결정되므로 저렇게 큰 크기의 list를 선언하면, 일반 컴퓨터의 한정된 용령의 메모리로는 감당할 수 없기 때문이다.

메모리의 크기가 가상 메모리를 사용한다고 해도 일반 컴퓨터는 어지간하면 100기가바이트를 넘지 않을텐데 그것을 상회하도록 크기를 크게 만든 자료구조는 생성할 수 없다.

 

하지만 저렇게 큰 크기의 값을 생성하는 generator은 문제없이 실행된다.

big_gen = (i for i in range(1, 10000000000000000000000000000+1))

# 문제없이 바로 생성됨

심지어 지금 만든 generator은 앞선 big_list보다 생성할 값의 크기가 훨씬 더 큽니다.

 

 

이렇게 매우 큰 크기를 갖는 자료구조를 다룰 일이 현업에서는 생각보다 더 흔하다고 합니다.

매우 큰 파일을 읽는다거나, 네트워크를 통해 대용량의 데이터를 다운로드할 때를 생각해보면, 줄 등의 한 단위를 필요할 때 한 줄씩 평가하는 (evalute) lazy loading이 한번에 모든 데이터를 loading하는 방식보다 안전한 것은 당연합니다.

 

 

 

정리하자면, iterable, iterator, generator의 장점은 일련의 값을 사용할 때 모든 값을 메모리에 모두 로딩하는 대신 한 값씩 필요할 때마다 로딩, 평가함으로써(lazy loading, evaluate)

 

 

  1. 메모리를 효율적으로 사용할 수 있다.
    • list 는 리스트 안의 모든 데이터를 메모리에 저장하기 때문에 list 의 크기만큼 메모리 용량을 사용하게 됩니다..
    • 반면에 generator 는 데이터 값을 모두 저장하는 것이 아니고 next() 메소드를 통해 값에 접근할 때마다 메모리에 저장하는 방식이어서 메모리 효율이 좋다는점.
  2. Lazy evaluation( 말그대로 실행을 지연시킨다는 의미)을 통해 메모리 부족으로 프로그램이 실패하는 것을 방지함으로써 보장되는 안정성!!.

 

 

 

 

참조:

offbyone.tistory.com/83

 

hackersstudy.tistory.com/9

 

nirsa.tistory.com/118

 

velog.io/@wltjs10645/Python-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0

 

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

반응형

'python' 카테고리의 다른 글

[python] modul(모듈)  (0) 2021.02.23
[python] 가변 인자(*args, **kwargs)  (0) 2021.02.22
[python] lambda expression(람다 표현식)  (0) 2021.02.19
[python] Decorator(데코레이터)  (0) 2021.02.18
[python] Closure(클로저 )  (0) 2021.02.17

+ Recent posts