16. 코루틴

 

Remarks

이 글은 전문가를 위한 파이썬(Fluent Python)을 정리한 자료입니다.


1. Coroutine

  1. 기본적인 구현
     def simple_coroutine():
         print("-> coroutine started")
         x = yield
         print("-> coroutine received:", x)
    
     my_coro = simple_coroutine()
     next(my_coro)
     # -> coroutine started
    
     my_coro.send(42)
     # -> coroutine received: 42
     # ---------------------------------------------------------------------------
     # StopIteration                             Traceback (most recent call last)
     # Cell In[4], line 1
     # ----> 1 my_coro.send(42)
    
     # StopIteration: 
    
  2. 코루틴은 네 가지 상태를 가진다. 상태는 inspect.getgeneratorstate()를 통해 알 수 있다.
    1. GEN_CREATED: 대기 상태
    2. GEN_RUNNING: 실행 중인 상태
    3. GEN_SUSPENDED: yield 문에서 대기하고 있는 상태
    4. GEN_CLOSED: 실행이 완료된 상태
  3. 처음 next(coro)를 호출해서 코루틴이 호출자로부터 값을 받을 수 있도록 첫 yield문까지 실행을 진행시키는 작업을 priming(기동)한다고 표현한다.
  4. 예제: 이동 평균을 계산하는 코루틴
     def averager():
         total = 0.0
         count = 0
         average = None
         while True:
             term = yield average
             total += term
             count += 1
             average = total / count
    
     coro = averager()
     next(coro)
     for i in range(5):
         result = coro.send(i)
         print(result)
    
     # 0.0
     # 0.5
     # 1.0
     # 1.5
     # 2.0
    
    1. next(coro): 첫 yield 까지 실행하고, average를 생성
       total = 0.0
       count = 0
       average = None
       while True:
                  yield average
      
    2. coro.send(i): yield 문의 값이 i로 평가되고 이 값이 term에 할당된다.
           term = yield
           total += term
           count += 1
           average = total / count
      

      그리고 다음 yield 문까지 실행시켜 average를 생성하고 대기(GEN_SUSPENDED)

       while True:
                  yield average
      
  5. 코루틴을 기동하기 위한 데커레이터
     from functools import wraps
    
     def coroutine(func):
         @wraps(func)
         def primer(*args, **kwargs):
             gen = func(*args, **kwargs)
             next(gen)
             return gen
         return primer
    

    next()가 실행된 코루틴이 반환되기 때문에 GEN_SUSPENDED 상태로 나오게 된다.

  6. 코루틴 종료와 예외 처리
     class DemoException(Exception):
         pass
    
     def demo_exc_handling():
         print("-> coroutine started")
         while True:
             try:
                 x = yield
             except DemoException:
                 print("*** DemoException handled. Continuing...")
             else:
                 print("-> coroutine received: {!r}".format(x))
         raise RuntimeError("This line should never run.")
    
    
     exc_coro = demo_exc_handling()  # GEN_CREATED
     next(exc_coro)                  # GEN_SUSPENDED
     exc_coro.throw(DemoException)   # GEN_SUSPENDED
     # exc_coro.throw(Exceotion)     # GEN_CLOSED
     exc_coro.close()                # GEN_CLOSED
    
  7. 코루틴에서 값 반환하기
     from collections import namedtuple
    
     Result = namedtuple("Result", "count average")
    
     def averager():
         total = 0.0
         count = 0
         average = None
         while True:
             term = yield
             if term is None:
                 break
             total += term
             count += 1
             average = total/count
         return Result(count, average)
    
     coro_avg = averager()
     next(coro_avg)
    
     coro_avg.send(10)
     coro_avg.send(30)
     coro_avg.send(6.5)
     try:
         coro_avg.send(None)
     except StopIteration as exc:
         result = exc.value
         # Result(count=3, average=15.5)
    
  8. yield from 사용하기
    1. 반복형 객체를 연결할 수 있다.
       def gen():
       #    for c in "AB":
       #        yield c
           yield from "AB"
      
    2. 코루틴 위임(coroutine delegation)
      1. 용어 정리
        • 대표 제너레이터(delegating generator): yield from <interator> 표현식을 담고 있는 제너레이터 함수
        • 하위 제너레이터(subgenerator): yield from 표현식 중 <interator>에서 가져오는 제너레이터
        • 호출자(caller): 대표 제너레이터를 호출하는 코드
      2. 예제
        • 머리가 터질 수 있음 ```python from collections import namedtuple from pprint import pprint

        Result = namedtuple(“Result”, “count average”)

        # subgenerator def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total / count return Result(count, average)

        # delegating generator def grouper(results, key): while True: results[key] = yield from averager()

        # caller def main(data): results = {} for key, values in data.items(): group = grouper(results, key) next(group) for value in values: group.send(value) group.send(None) # print(results) report(results)

        def report(results): pprint(results) # for key, result in sorted(results.items()): # group, unit = key.split(“;”) # print(f”{group:2} {result.count:5} {result.average:10.2f} {unit}”)

        data = { “girls;kg”: [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], “girls;m”: [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], “boys;kg”: [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], “boys;m”: [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], }

        main(data)

        {‘boys;kg’: Result(count=9, average=40.422222222222224), ‘boys;m’: Result(count=9, average=1.3888888888888888), ‘girls;kg’: Result(count=10, average=42.040000000000006), ‘girls;m’: Result(count=10, average=1.4279999999999997)} ```

      3. yield from 구현 안에 next()가 포함되어 있기 때문에 외부에서 먼저 priming 하면 안 된다.