3 분 소요

이터레이터와 제네레이너 이런건 도대체 뭘까?

/ 이터레이터 / 제네레이터 / yield /

이터레이터

  • 컬렉션 타입
    •  list, tuple, set, dictionary와 같이 여러개의 요소(객체)를 갖는 데이터 타입
  • 시퀀스 타입
      • list, tuple, range, str등과 같이 순서가 존재하는 데이터 타입

Iterable VS Iterator

내부 요소(member)를 하나씩 리턴할 수 있는 객체를 보고 Iterable하다. 쉽게 생각하면 우리가 평소에 많이 사용하는 for문을 떠올리면 된다.

_list = [1,2,3,4,5]

for i in _list:
    print(i)

이렇게 for문을 통해 순회할 수 있는 객체를 Iterable하다고 생각하시면 된다. 대표적으로 위에서 잠깐 설명한 시퀀스 타입과 컬렉션 타입의 객체가 있다.

그럼 Iterable한 것과 Iterator는 무슨 차이가 있는걸까?

  • 쉽게 말하자면 Iterable한 것은 __next__ 메소드가 존재하지 않고 Iterator는 존재한다고 생각하면 된다.
  • 즉 __next__ 메소드로 다음 값을 반환할 수 있으면 Iterator, 없으면 Iterabe한 객체다.

더 자세한건 아래 링크 출처 : https://tibetsandfox.tistory.com/28

제네레이터

제너레터(Generator)란?

  • 제너레이터는 쉽게 설명해서 이터레이터(Iterator)*를 생성하는 객체라고 할 수 있다. 즉 모든 제너레이터는 이터레이터에 속한다.** 
  • 컴프리헨션(Comprehension) 문법 혹은 함수 내부에 yield 키워드를 사용함으로써 만들 수 있다.

제너레이터 만들기

우선 컴프리헨션 문법으로 만들어보자.

foo = (i for i in range(1, 6))
print(type(foo)) # <class 'generator'>
  • 이렇게 컴프리헨션 문법을 통해 생성된 제너레이터를 제너레이터 표현식이라고 칭한다.
    • []가 아니라 ()를 사용했음에 주의.

yield 키워드를 이용해 만들어보자.

def func():
    for i in range(1, 6):
        yield i

foo2 = func()
print(type(foo2)) # <class 'generator'>
  • 이렇게 생성된 foo와 foo2 모두 next 메소드를 통해 값에 접근할 수 있다.
  • 또한 제너레이터는 이터레이터에 속하기 때문에 불러올 값이 더 이상 없으면 StopIteration 예외를 발생시킨다.

yield 키워드

  • yield 키워드는 그럼 어떻게 동작하는걸까?
  • 언뜻 보기엔 return과 비슷하게 동작하는 것 같지만 큰 차이가 있다.
def func():
    yield 1
    yield 2
    yield 3

foo = func()

print(next(foo)) # 1
print(next(foo)) # 2
print(next(foo)) # 3
  • 제너레이터 함수는 yield를 통해 값을 리턴합니다.
  • next메소드나 for문 순회 등을 통해 저 값들을 리턴받을 수 있는데 중요한 점은 yield가 호출된다고 해서 함수가 종료되는 것이 아니라는 것
  • yield는 호출되면 값을 반환하고 그 시점에서 함수를 잠시 정지시킨다. 그리고 다음 next가 호출되면 정지된 시점부터 다시 로직을 실행한다.
  • return은 호출되면 함수를 종료한다. yield와 return의 차이가 바로 이것
  • 제너레이터 내부에서 return을 사용 시 현재 값에 상관없이 무조건 StopIteration 예외가 발생
def func():
    print("1 리턴 전")
    yield 1
    print("1 리턴 후, 2 리턴 전")
    yield 2
    print("2 리턴 후, 3 리턴 전")
    yield 3
    print("3 리턴 후")


foo = func()

print(next(foo))
print(next(foo))
print(next(foo))

이해를 돕기 위해 각 yield 사이에 print문을 삽입

1 리턴 
1
1 리턴 , 2 리턴 
2
2 리턴 , 3 리턴 
3

이렇게 제너레이터 내에 다양한 로직을 추가할 수 있다는 점에서 yield 는 유용하다.

제너레이터의 체크포인트


yield from

  • yield로 iterable한 객체의 요소를 하나씩 리턴해 줄 수도 있다.
  • 이 경우 yield에 더해 from이라는 키워드를 사용한다.
    def func():
      _list = [1, 2, 3, 4, 5]
      for i in _list:
          yield i
    

즉 위와 같이 리스트를 순회하며 각각 값을 yield로 반환하는 대신,

def func():
    _list = [1, 2, 3, 4, 5]
    yield from _list

이렇게 for문 없이 yield로 반환하는 것도 가능

사용법은 위의 예제나 아래 예제나 같음

def generator(stop_number):
    num = 0
    while True:
        if num >= stop_number:
            return
        num += 2
        yield num


def func(stop_number):
    gen = generator(stop_number)
    yield from gen


foo = func(10)

추가로 이렇게 yield from 키워드로 제너레이터를 전달하는 것도 가능

위 예제는 10 이하의 짝수를 foo 객체의 next로 하나씩 꺼내올 수 있다.


게으른 제너레이터

제너레이터는 lazy하다. 이 특성때문에 제너레이터는 지연 평가(Lazy Evaluation) 구현체 라고 부르기도 함

lazy 하지 않은 객체, 가장 대표적으로 list를 예로 든다면

_list = [1,2,3,4,5]
  • list객체의 각 요소는 이미 값이 정해져 있다.

0번 인덱스 : 1

1번 인덱스 : 2

2번 인덱스 : 3 … 이런식

이런 객체는 몇 번째 어떤 요소가 있는지 손쉽게 확인할 수 있다는 장점이 있지만 그 크기가 커질 수록 메모리의 낭비가 심해진다는 단점이 있다.

만약 list의 길이가 100만이 넘는다면? 100만개가 넘는 값을 모두 기억해야 하니 메모리가 많이 필요하다.

  • 하지만 lazy한 객체는 모든 요소를 저장하고 있지 않고
  • 처음에 줘야 할 값, 그 다음부터 줘야 할 값, 값을 언제까지 줘야 하는지 에 대한 정보만을 가지고 있다.
    • 이 정보들을 바탕으로 각 호출마다 값을 반환하게 되는 것

정리하자면 모든 값을 메모리에 올려두지 않고 필요할 때(호출할 때) 값을 평가해서 반환하는 방식

이 특성을 이용한 재미있는 예시를 하나 보자

def endless_numbers():
    num = 2
    while True:
        yield num
        num = num + 2


foo = endless_numbers()

처음 반환할 값 : 2 (num의 초기값)

그 다음부터 줄 값 : num에 2를 더한 값

언제까지? : 명시되지 않았으므로 호출하는 만큼 계속

이렇게 생성된 foo 객체는 호출하는 만큼 짝수를 계속해서 반환 그리고 이 횟수는 이론상으로 100만, 1000만, 혹은 그 이상도 가능

같은 로직을 list로 구현하는것은 당연히 불가능 이것이 lazy한 객체의 특징이자 장점

따라서 불러올 다음 값에 대한 패턴이 존재한다면 lazy한 객체를 사용하는 것이 메모리 측면에서는 이득이라고 할 수 있다.

더 자세한건 아래 링크 출처 : https://tibetsandfox.tistory.com/28

댓글남기기