ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 제너레이터(Generator)
    Programming/python 2020. 6. 6. 22:26

    제너레이터를 설명하기 앞서 이터레이터에 대한 이해가 살짝 필요하다. 제너레이터랑 이 이터레이터에 yield 구문을 추가해 호출될 때마다 이터레이터와는 다르게 모든 값을 포함한 배열을 리턴해주는게 아니라 하나의 값만을 리턴하기 때문이다.

    이터레이터란?

    • 이터레이터(iterator)는 값을 차례대로 꺼낼 수 있는 객체(object)이다
    • 파이썬에서는 이터레이터만 생성하고 값이 필요한 시점이 되었을 때 값을 만드는 방식을 사용함
    • 데이터 생성을 뒤로 미루는 것인데 이런 방식을 지연 평가(lazy evaluation)라고 함
    • 이터레이터는 반복자라고 부르기도 함

    반복 가능한 객체(iterable)와 이터레이터(iterator)는 별개의 객체이므로 둘은 구분해야 한다.

    이터레이터와 제너레이터의 차이점

    • 이터레이터는 __next__ 메서드 안에서 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드의 반환값으로 나온다.
    • 이터레이터는 raise로 StopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 발생한다.

    이때문에 제너레이터는 이터레이터에 비해서 아주 작은 메모리를 필요로 한다.

    제너레이터를 왜 사용하는가의 물음에 대해서는 위의 비교적 적은 메모리를 사용한다는 걸로 대답을 대체할 수 있다.

    메모리를 효율적으로 사용할 수 있다는 이야기는 객체를 다루는 입장에서 아주 큰 메리트이기 때문이다.

    import sys
    
    print((sys.getsizeof([i for i in range(100)])))
    # 912
    print((sys.getsizeof((i for i in range(100)))))
    #120

    getsizeof로 리스트와 제너레이터로 만들어진 객체의 사이즈를 비교해봐도 이 사실을 확인 할 수 있다. (참고로() 를 사용하면 generator object로 만들 수 있다.)

    list는 안에 속한 데이터를 모두 메모리에 담기 때문에 list가 늘어날 수록 메모리 사이즈가 늘어나게 된다. 메모리는 공간적인 소모도 의미하지만 데이터가 커질 수록 프로그램의 속도 또한 저하될 수 있다. 따라서 더욱 객체가 큰 데이터를 다루는 프로그램일 수록 이 제너레이터의 효율성과 필요성은 높아지게 된다고 할 수 있다.

    제너레이터는 위처럼 () 로 씌워서 generator expression을 통해 간단하게 변환해서 사용할 수 있지만 직접 함수로 만들어서 사용할 수도 있다.

    제너레이터 만들기


    간단하게 0부터 2씩 오르는 제너레이터를 만들어보자.

    def generator(stop):
        n = 0              # 숫자는 0부터 시작
        while n < stop:    # 현재 숫자가 반복을 끝낼 숫자보다 작을 때 반복
            yield n        # 현재 숫자를 바깥으로 전달
            n += 2         # 현재 숫자를 증가시킴
    
    
    for i in generator(10):
        print(i)
    # 0
    # 2
    # 4
    # 6
    # 8 을 출력

    제너레이터의 핵심은 다른 객체들과 달리 하나씩 yield를 통해 가져온다는 것이므로 next 메서드를 사용해서 하나씩 가져올 수도 있다.

    def generator(stop):
        n = 0              
        while n < stop:    
            yield n        
            n += 2         
    
    
    a = generator(8)
    print(next(a))
    # 0
    print(next(a))
    # 2
    print(next(a))
    # 4
    print(next(a))
    # 6

    여기서 제너레이터의 핵심적인 특징이 등장한다. 제너레이터는 자신이 했던 일을 기억하면서 대기하고 있다가 다시 호출되면 전의 했던 일에서 이어서 다음 동작을 한다는 사실이다.

    이 사실을 조금만 응용한다면 하나의 함수가 하나의 일만 하는 것이 아니라 일반 함수로 여러 가지의 일을 담당하게 되는 훨씬 다용성이 높은 코드를 짤 수 있다는 것을 의미한다.

    yield from


    파이썬 3.3 이상부터는 yield from이라는 구문을 제공해서 yield에서 더 나아가 추가적으로 iterable한 객체를 한꺼번에 바깥으로 전달할 수 있게되었다.

    여기서 iterable이라는 것은 말 그대로 반복가능한 객체를 이야기한다. 이 반복 가능한 객체들은 리스트, 튜플, range, 문자열, 딕셔너리(dict), 세트(set)가 있다.

    반복 가능한 객체

    • 반복 가능한 객체는 말 그대로 반복할 수 있는 객체인데 우리가 흔히 사용하는 문자열, 리스트, 딕셔너리, 세트가 반복 간으한 객체이다.
    • 요소가 여러 개 들어있고, 한 번에 하나씩 꺼낼 수 있는 개체이다.
    • 객체가 반복 가능한 객체인지 알아보는 방법은 객체에 __iter__ 메서드가 들어있는지 확인해보면 된다.

    쉽게 for문에 넣어서 반복문을 돌릴 수 있는 말그대로 반복가능한 객체들을 이야기한다.

    이중에서 리스트와 튜플, range, 문자열은 순서가 존재하는 시퀀스 객체로 따로 분류된다. 딕셔너리와 세트는 요소의 키의 순서가 정해져있지 않아서 반복 가능한 객체는 맞지만 시퀀스 객체는 아니다.

    yield form을 이용해서 위의 반복가능한 리스트나 튜플 등과 같은 객체들을 한꺼번에 전달할 수 있게된다는 것이 핵심이다.

    def generator():
        n = [1, 2, 3]
        yield from n  # 리스트 안에 있는 요소를 바깥으로 전달
    
    
    for i in generator():
        print(i)
    # 1
    # 2
    # 3 이 출력된다.

    하나씩 바깥으로 꺼내는 개념은 같지만 리스트를 하나씩 꺼내서 던지는 과정을 생략하기 때문에 코드가 훨씬 간결해지고 가독성을 올려준다.

    댓글

Copyright 2023. 은유 All rights reserved.