Skip to content

2.13. 내용정리: 13일차

흔한 찐따 edited this page Mar 19, 2022 · 8 revisions

이터레이터 (Iterator)

  • 이터레이터란, '반복자'라는 의미이다.
  • 파이썬에서 반복자는 여러 개의 요소를 가지는 컨테이너( list , tuple , set , dict, str )에서 각 요소를 하나씩 꺼내서 어떤 처리를 수행하는 간편한 방법을 제공하는 객체를 의미한다.
  • 반복문인 for 문은 먼저 주어진 컨테이너 객체에 대해 iter 메서드를 호출해서 이터레이터 객체를 구한다.
    • for 문 사용 시 주로 하나의 요소를 가져올 때 관례적으로 변수명을 i 라고 표현하는데, 이는 iterator 의 약어라는 이야기가 있다.
  • 그리고 나서 내부의 요소를 하나씩 가져오기 위해서 __next__() 메서드를 호출한다.
    • __next__ 메서드는 하나의 요소를 반환하고 다음 요소를 가리킨다.
    • 더 이상 가져올게 없으면 StopIteration 예외를 발생시킨다.
  • 이터레이터에 대해서 다음 요소를 직접 가져오기 위해서는 내장 함수인 next 함수를 통해 가져올 수 있다.

예시

  • 아래처럼 for 문을 사용할 때 요소 iiterator 객체이다.
elements = [1, 2, 3, 4, 5]
for i in elements:
    print(i)
  • dict 타입인 경우, items 메서드를 통해 key 값과 value 값의 쌍을 각각 동시에 가져올 수 있다.
d = { 'a': 1, 'b': 2, 'c': 3 }
for key, value in d.items():
    print(key, ':', 'value')
  • iter 함수를 통해 이터레이터 객체로 변환시킬 수 있다.
  • iter 함수를 통해 만들어진 이터레이터 객체는 next 함수를 통해 다음 요소를 가져올 수 있다.
s = 'abc'
i = iter(s)

# <class 'str_iterator'>
print(type(i))

a = next(i)
print(a)

b = next(i)
print(b)

c = next(i)
print(c)

# 더 이상 요소가 없으므로, 'StopIteration' 예외가 발생한다.
d = next(i)
print(d)

제너레이터 (Generator)

  • 제네레이터는 튜플 타입을 컴프리헨션 방식으로 선언하면 생성된다.
  • 메모리 주소값을 활용하므로 속도가 굉장히 빠르며, 메모리를 아낄 수 있다는 장점이 있다.
  • next 함수를 통해 첫번째부터 값을 하나씩 차례대로 불러낼 수 있다.
  • next 함수를 통해 불러낸 첫번째 값은 제네레이터 내에서 값이 삭제되며, 메모리에서 해제된다.

예시

  • 튜플 타입을 컴프리헨션 방식으로 선언하는 경우
t = (i for i in range(1, 11))
print(t)
print(type(t))
  • 함수의 인자값으로 튜플 타입을 컴프리헨션 방식으로 넘기는 경우
s = sum(i for i in range(1, 11))
print(s)

이와 같은 제너레이터 표현식은 함수의 인자로 즉시 사용되는 상황을 위해 디자인되었다.

yield

함수 안에서 yield 키워드를 사용하면 함수는 제너레이터 가 되며, yield 에는 값(변수)을 지정한다.

예시

def generator():
    yield 0
    yield 1
    yield 2

# 함수를 호출하게 되면 제너레이터 객체가 반환된다.
gen = generator()

# <generator object generator at 0x...>
print(type(gen))

for i in gen:
    print(i)

제너레이터 객체의 __next__ 를 호출해보면, 숫자 0 , 1 , 2 가 나오다가 StopIteration 예외가 발생한다.

a = gen.__next__()
print(a)

b = gen.__next__()
print(b)

c = gen.__next__()
print(c)

# 'StopIteration' 예외 발생
d = gen.__next__()
print(d)

즉, next 함수는 이터레이터 객체의 __next__ 메서드를 호출하는 함수라는 것을 알 수 있다.

yield 키워드의 원리

  • yield 를 사용하여 외부로 전달한 값은 next 함수(즉, __next__ 메서드)의 반환값으로 나온다.
  • 따라서 위의 예제에서 next(gen) 의 반환값을 출력해보면 yield 에 지정한 값 0 , 1 , 2 가 차례대로 나온다.
  • 즉, 제너레이터 함수가 실행되는 중간에 next 로 값을 가져온다.

yield 키워드의 동작 과정

  1. 먼저 gen = generator() 와 같이 제너레이터 객체를 만든다.
  2. 그 다음, next(gen) 을 호출하면 제너레이터 안의 yield 0 이 실행되어 정수 0 을 전달한 뒤, 외부의 코드가 실행되도록 양보한다.
  3. 함수 외부에서는 print(a)next(gen) 에서 반환된 값을 출력한다.
  4. 값을 출력했으면 next(gen) 으로 다시 제너레이터 안의 코드를 실행한다.
  5. 이때는 yield 1 이 실행되고, 정수 1 을 발생시켜서 함수 외부로 전달한다.
  6. 그리고 함수 외부에서는 print(b)next(gen) 에서 반환된 값을 출력한다.
  7. 위와 같은 과정을 반복한다.
  8. 더 이상 yield 를 통해 발생시킬 데이터가 존재하지 않는 경우, StopIteration 예외를 발생시킨다.

yield 키워와 return 키워드

  • 제너레이터는 함수 끝까지 도달하면 StopIteration 예외가 발생한다.
  • 마찬가지로 함수 내부의 return 키워드는 함수를 즉시 끝내므로, return 을 사용해서 함수 중간에 빠져나오면 StopIteration 예외가 발생한다.
  • 특히 제너레이터 안에서 return 에 반환값을 지정하면 StopIteration 예외의 에러 메시지로 들어간다.

예시

def generator():
    yield 1
    yield 2
    return '더 이상 값이 존재하지 않습니다.'
 
# 제너레이터 생성
gen = generator()

# 'next' 함수를 통해 다음 값을 꺼내온다.
next(gen)
next(gen)

# 아래의 코드가 실행될 경우, 'StopIteration: 더 이상 값이 존재하지 않습니다.' 라는 메시지가 출력된다.
next(gen)

응용

무한 루프를 통해 next 함수로 계속해서 값을 가져오는 것이 가능하다.

def generator():
    # 계속 참이 성립되므로, 무한 루프가 성립된다.
    x = 0
    while True:
        x += 1
        yield x

gen = generator()

# 'next' 함수를 통해 계속해서 값을 불러와도 'StopIteration' 예외가 발생되지 않는다.
a = next(gen)
print(a)

b = next(gen)
print(b)

c = next(gen)
print(c)

d = next(gen)
print(d)
...

제너레이터를 사용하는 이유

  • 앞서 서술했듯, 제너레이터는 메모리 주소값을 활용하므로 속도가 굉장히 빠르며, 메모리를 아낄 수 있다는 장점이 있다.
  • 무엇보다도 제너레이터를 사용하는 궁극적인 이유는 제너레이터가 생산자-소비자(producer-consumer) 문제 와 밀접한 연관이 있기 때문이다.

생산자-소비자(producer-consumer) 문제

위키에 검색해보면 생산자-소비자 문제에 대해서 다음과 같이 정의하고 있다.

  • 생산자-소비자 문제란, 여러 개의 프로세스를 어떻게 동기화할 것인가에 관한 고전적인 문제이다.
  • 한정 버퍼 문제(bounded-buffer problem) 라고도 한다.

이를 좀 더 알기 쉽게 풀어서 정리해보면 다음과 같다.

  1. 유한한 개수의 물건(데이터)을 임시로 보관하는 보관함(버퍼)에 여러 명의 생산자들과 소비자들이 접근한다.
  • 버퍼(buffer) 란, 한 곳에서 다른 곳으로 데이터를 이동할 때, 임시적으로 그 데이터를 저장하기 위해 사용되는 물리적인 메모리 저장소의 영역을 의미한다.
  1. 생산자는 물건이 하나 만들어지면 그 공간(버퍼)에 저장한다.
  2. 이때 저장할 공간이 없는 문제가 발생할 수 있다.
  3. 소비자는 물건이 필요할 때 보관함에서 물건을 하나 가져온다.
  4. 이 때는 소비할 물건이 없는 문제가 발생할 수 있다.

이를 요약하자면 아래와 같다.

  • 데이터의 개수는 유한하며, 한정적이다.
  • 컴퓨터 메모리 저장소 공간 중에는 버퍼라는 곳이 존재하는데, 컴퓨터에서는 데이터를 버퍼라는 곳에 저장한다.
  • 컴퓨터의 메모리 공간은 한정적인데, 그 공간에 저장될 수 있는 요소들은 한정적일 수 밖에 없다.
  • 메모리 공간이 가득찬 상태인 경우, 데이터를 생산하는 쪽(생산자)이 데이터를 더 이상 생산할 수가 없다.
  • 반대로 데이터가 없어서 메모리 공간이 비어있는 경우, 데이터를 사용하는 쪽(소비자)이 데이터를 더 이상 소비할 수가 없다.

해결 방법

  • 생산자와 소비자를 상호 배타적(exclusive) 관계 로 만들어야 한다.
    • 이해하기 쉽게 수식으로 표현하자면, 생산자를 P , 소비자를 C , 공통 집합을 A 라고 가정하면 아래와 같다.
    • P ∩ C = 0 이며, 동시에 (P, C) ⊂ A 인 상태와 같다.
  • 이 문제를 해결하는 것을 생산자-소비자 협동 이라고 하며, 버퍼가 동기화되어 정상적으로 동작하는 상태(즉, 상호 배타적 관계)를 의미한다.
  • 문제를 해결하기 위해 세마포어(Semaphore) 를 활용할 수 있다.
    • 세마포어란, 두 개의 원자적(Atomic) 함수로 조작되는 정수 변수로서, 멀티프로그래밍 환경에서 공유 자원에 대한 접근을 제한하는 방법으로 사용된다.
    • 공유된 자원의 데이터 혹은 임계영역(Critical Section) 등에 여러 프로세스(process) 혹은 쓰레드(thread)가 접근하는 것을 막아준다. (즉, 동기화 대상이 하나 이상)
    • 쉽게 말해, 하나의 데이터를 동시에 여러 곳에서 접근하여 사용하려고 하는 것을 방지해주는 역할을 하는 것이 바로 세마포어다.

제너레이터와의 관계

앞서 이 문제점의 전제를 다시 한번 살펴보면 다음과 같다.

  • 컴퓨터 메모리 공간은 한정적이다.
  • 데이터는 유한하다.

그리고 제너레이터의 특징을 살펴보자.

  • 메모리 주소값을 활용하므로 속도가 굉장히 빠르며, 메모리를 아낄 수 있다는 장점이 있다.
  • next 함수를 통해 첫번째부터 값을 하나씩 차례대로 불러낼 수 있다.
  • next 함수를 통해 불러낸 첫번째 값은 제네레이터 내에서 값이 삭제되며, 메모리에서 해제된다.

즉, 제너레이터는 데이터를 튜플 컴프리헨션이나 yield 키워드를 통해 데이터를 생산하며, next 함수를 통해 사용한 후에는 메모리에서 즉시 해제된다.

흔한 찐따

안녕하세요, 흔한 찐따 입니다.

Clone this wiki locally