Python) Yield 에 대해서 알아보기

2021. 8. 7. 22:41분석 Python/구현 및 자료

yield에 대해서 생각보다 많이 쓰는 것 같은데, 너무 모르는 것 같아서 정리해보고자 합니다.

 

yield는 주로 Generator에서 쓰이게 되며,  Python Generator를 정의하는 핵심은 "yield" 키워드를 사용하는 것이라고 합니다.

Python  Generator는 대규모 collection이 필요한 시나리오 및 멀티 스레딩과 같은 기타 특정 시나리오 및 코드의 가독성을 향상하는 시나리오에서 널리 사용됩니다.

 

이 글에서 모든 것을 다룰 수는 없겠지만 기본적인 개념이나 고급 사용 방법에 대해서 적어보고자 합니다.

 

일단 generator를 알아보고자 합니다.

 

목차

    Iterator란?

     

    그 전에 알아야 하는 것은 Iterator입니다.

     

    Iterator란 반복 가능한 객체, 즉 반복문을 이용해서 데이터를 순회하면서 처리하는 것을 의미합니다

    iterator는 next() 메서드로 데이터를 순차적으로 호출 가능한 객체입니다.

     

    x = ["a","b","c"]
    y = iter(x)
    next(y) # a 
    next(y) # b
    next(y) # c

    비슷한 말로는 iterable이 있다. 헷갈릴 수 있으니 적어보면 다음과 같습니다.

    iterable이라는 것은 리스트를 만든 후 해당 리스트에 있는 객체를 순환하며 하나씩 꺼내서 사용할 수 있다고 하고 이러한 과정을 Iteration이라고 합니다. (iterable에 대표적인 것은 list , str , tuple이다)

    x = ["a","b","c"]
    for i in x :
        print(i)

     

    Generator란?

     

    Generator란 Iterator를 생성해주는 함수를 의미합니다. Generator는 모든 값을 메모리에 담고 있지 않고, 그때그때 값을 생성해서 반환하기 때문에 제너레이터를 사용할 때는 한 번에 한 개의 값만 순환할 수 있습니다.

     

    포인트는  모든 값을 포함하여 변환하는 대신 호출할 때마다 한 개의 값을 리턴한다는 것입니다.

    즉, 그때그때 생성을 하니 메모리가 충분하지 않은 상황에서 대용량의 반복 가능한 구조로 순회할 수 있다는 것입니다.

     

    yield를 같이 사용하면 Generator를 만들 수 있습니다.

    yield가 호출되면 임시적으로 return이 호출되며, 한번 더 실행되면 실행되었던 이전 yield의 다음 코드가 실행됩니다.

     

     그래서 아래의 예제를 보면, 처음 yield와 뒤의 yield 사이에 print가 있다고 했을 때 출력 결과가 다음 코드를 실행하는 것을 알 수 있습니다.

    def generator():
        yield "a"
        print("aa")
        yield "b"
        print("bb")
        yield "c"
    gen = generator()
    gen
    # <generator object generator at 0x7f5c74734350>
    next(gen) # a
    next(gen) # aa , b
    next(gen) # bb , c

     

    names = ['Alice', 'Bob', 'Chris', 'David', 'Emily']
    def gen_roster(names):
        for name in names:
            yield name
    roster = gen_roster(names)
    for name in roster:
        print(name)

    메모리도 실제로 보면 엄청난 차이가 발생합니다

    import sys
    
    a_list = [1 for i in range(10000000) ]
    a_generator = (1 for i in range(10000000) )
    
    sys.getsizeof(a_list) # 81528064 Bytes
    sys.getsizeof(a_generator) # 112 Bytes

     

    Generator

    NEXT

    위에서 적은 앞의 예에서 for 루프에서 생성기를 사용하는 것은 그다지 의미가 없습니다.

    Generator를 사용하는 이점은 한 번에 하나의 값을 얻을 수 있다는 것입니다.

    이것은 거대한 컬렉션을 다룰 때 매우 유용할 수 있습니다.

    즉, Generator를 만들 때 항목이 메모리로 읽히지 않습니다. 다음 항목을 가져오려고 할 때 yield 키워드를 누르는 경우에만 항목이 생성됩니다.

     

    따라서 반드시 생성기의 "NEXT" 요소를 얻는 것이 중요하다. 이 경우 __next__() 함수를 사용할 수 있습니다.

    roster = gen_roster(names)
    roster.__next__()
    # Alice'
    next(roster)
    # 'Bob'

     

    for 루프는 yield 키워드에 도달할 때까지 한 번만 실행된다고 생각할 수 있습니다. 

    이렇게 한 개씩 이동하는 것이 끝나면 에러로는 StopIteration이 발생합니다.

     

    roster는 현재 로테이션에 대해 누가 대기 중인지 결정하는 데 사용되기 때문입니다.

    즉, 모든 이름이 다시 반복되도록 "cursor"가 처음으로 돌아가려면 어떻게 해야 할까?

     

     

    def gen_roster(names):
        while True:
            for name in names:
                yield name
    roster = gen_roster(names)
    for i in range(12):
        print(next(roster))
          
    Alice
    Bob
    Chris
    David
    Emily
    Alice
    Bob
    Chris
    David
    Emily
    Alice
    Bo

     

    Send a Value

    내가 이 글을 쓰게 된 목적이라고도 할 수 있는데, 실제 코드에서 send를 보고 당황한 적이 있습니다.

    그러다가 어떻게든 이해를 하게 됐지만, 다시는 까먹지 않게 위해 정리를 시작하게 되었습니다.

     

    Generator의 고급 사용법 중 하나는 Generator에 값을 보내는 것입니다.

    이 값은 현재 yield 표현식의 결과가 되며 메서드는 Generator가 생성한 next을 반환합니다.

     

    따라서 다음 값을 반환할 것이기 때문에 생성기가 방금 보낸 값을 반환할 것으로 기대하면 안 됩니다.

    그러나 이것을 사용하여 생성기 내부에서 작업을 수행할 수 있습니다. 예를 들어, 무한 생성기에 특정 값을 전송하여 무한 생성기를 중지할 수 있습니다.

     

    아래 예제에서는 무한 rotation generator에 특정 인덱스에서 roster에 stop이라는 값을 보내줘서 생성기를 멈춥니다. 

    그래서 평상시에는 None 값을 밥다가 i=3일 때 stop을 받아서 멈추게 됩니다.

    그래서 3개까지는 받고 4번째부터 멈춘 것이라고 할 수 있습니다.

     

    def gen_roster(names):
        while names:
            for name in names:
                current_name = yield name
                if current_name == 'stop':
                    names = None
                    break
    
    roster = gen_roster(names)
    for i in range(10):
        if i == 3:
            roster.send('stop')
        print(next(roster))

     

    send 메서드는 multi threading 프로그래밍 시나리오에서 생성기의 동작이나 규칙을 변경하려는 경우에 매우 유용합니다.

     

    Stop a Generator - Throw Exception and Close

    문제가 있는 경우 throw() 메서드를 사용하여 Generator가 일시 중지된 지점에서 예외를 발생시킬 수 있습니다.

    오류 유형을 사용자 정의할 수 있습니다. 이 튜토리얼의 데모 목적을 위해 편의상 "TypeError"를 사용하겠습니다.

    roster = gen_roster(names)
    next_name = roster.throw(TypeError, 'Stop!')
    
    TypeError: Stop!
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    /tmp/ipykernel_3046911/815557122.py in <module>
          1 roster = gen_roster(names)
    ----> 2 next_name = roster.throw(TypeError, 'Stop!')
    
    /tmp/ipykernel_3046911/3167137861.py in gen_roster(names)
    ----> 1 def gen_roster(names):
          2     while names:
          3         for name in names:
          4             current_name = yield name
          5             print(current_name)
    
    TypeError: Stop!

    아무 문제가 없지만 여전히 Generator를 종료하고 싶다면 제너레이터의 close() 메서드를 사용할 수 있습니다. 이것은 무한 생성기가 있고 특정 지점에서 멈추고 싶을 때 매우 유용합니다.

     

    roster = gen_roster(names)
    for i in range(10):
        if i == 3:
            roster.close()
        print(next(roster))

     

    큰 데이터를 다룰 때 단순히 리스트가 아닌 generator를 만들어서 시도해봐야겠다.

     

     

     

    https://towardsdatascience.com/how-to-use-yield-in-python-5f1fbb864f94

     

    How To Use “yield” in Python?

    Python Generator — from basic to advanced usage

    towardsdatascience.com

    https://tech.ssut.me/what-does-the-yield-keyword-do-in-python/

     

    Python의 yield 키워드 알아보기

    이 글은 Stackoverflow "What does the yield keyword do in Python? (Python에서 yield 키워드는 무엇을 하나요?)"의 번역문입니다. 예재를 포함한 원문은 링크에서 확인해보실 수 있습니다. Python의 yield 키워드 알아

    tech.ssut.me

    https://scipy-lectures.org/advanced/advanced_python/index.html

     

    2.1. Advanced Python Constructs — Scipy lecture notes

    A context manager is an object with __enter__ and __exit__ methods which can be used in the with statement: In other words, the context manager protocol defined in PEP 343 permits the extraction of the boring part of a try..except..finally structure into a

    scipy-lectures.org

     

    728x90