일반함수를 비동기 코루틴화하는 데코레이터 만들기

지난 글에서 urlopen()과 같은 표준 라이브러리 함수를 어떻게 비동기 코루틴처럼 asyncio에서 사용할 수 있는지 살펴보았다. aiohttp 등의 비동기 라이브러리를 사용해서 여러 핸들러를 작성해야 할 때, 이와 같은 처리를 많이 해야 한다면 빈번하게 런루프 메소드를 호출하는 것보다, 간단히 데코레이터를 만들어서 활용하는 것이 어떨까? 무엇이 되었든 파이썬 함수는 인자를 받고 결과를 내보내는 구조로 되어 있다. ( (*args, **kwds) -> Result  ) 물론 상황에 따라 인자는 생략되기도 하고 결과는 암시적으로 None 이 될 것이다. 따라서 이러한 함수를 데코레이터와 함께 작성하여 별도의 스레드에서 실행되는 비동기 코루틴으로 만들어보도록 하자.

  1. 런루프 및 executor는 생략되는 경우 기본 런루프와 디폴트 executor를 사용하지만, 명시적으로 넘겨지는 경우 그것을 사용한다.
  2. 위 조건은 데코레이터 선언 자체에 들어가야 한다. 따라서 우리가 작성해야하는 함수는 데코레이터가 아니라 데코레이터 생성함수이다.
  3. run_in_executor() 메소드는 키워드 인자를 넘겨주지는 못한다. 따라서 functools.partial 을 사용해서 키워드인자를 만들어주어야 한다.

여차저차해서 코드는 다음과 같이 작성될 수 있다.

from functools import partial, wraps
import asyncio

def run_async(loop=None, pool=None):
  _loop = loop if loop is not None else\
          asyncio.get_event_loop()
  def decorator(fn):
    @wraps(fn)
    async def wrapped(*args, **kwds):
      _fn = partial(fn, **kwds)
      result = await _loop.run_in_executor(pool, *args)
      return result
    return wrapped
  return decorator

실제 사용은 이런식으로 한다.

import sqlite3 as sql

@run_async()
def get_users(page=1, limit=100):
  conn = sql.connect(database)
  c = conn.execute('''SELECT * FROM users LIMIT = ? OFFSET = ?''', 
                   (limit, (page-1) * limit))
  return c.fetchall()

async def run():
  fs = {get_users(i) for i in range(1, 11)}
  asyncio.

yield from – 다른 제너레이터에게 작업을 위임하기

제너레이터 내부에서 yield 가 쓰일 때, .send() 통해서 내부로 전달된 값으로 평가될 수 있다고 하였다. 만약, 해당 제너레이터/코루틴이 매 입력을 직접 처리하지 않고 다른 제너레이터에게 일부 작업을 위임할 필요가 있다면 yield from 이라는 새로운 문법을 사용할 수 있다. 이 문법을 사용할 경우 다음과 같이 처리된다.

  1. yield from sub_generator()의 형태로 쓰인다.
  2. 부모 제너레이터의 send(x) 가 호출되면 이 값은 자식 제너레이터에게 전달된다.
  3. 자식 제너레이터가 yield 한 값은 다시 부모 제너레이터가 외부로 전달해준다. 하지만 실행 흐름은 더 이상 진척되지 않는다.
  4. 자식 제너레이터가 return 하는 경우 최종적으로 부모 제너레이터의 yield 가 평가되고 부모 제너레이터가 흐름을 재개한다.

PEP 문서에서 다음의 예제를 소개하고 있다. 주어진 값의 누계를 계산하는 제너레이터가 있다. 그리고 이 제너레이터에게 누계를 위임하는 코루틴이 있다. 이를 사용하여 일련의 수열의 누적합을 계산해보자.

def accumulate():
  total = 0
  while True:
    next = yield total
    if next is None:
      return total
    total += next

def gather(ls):
  while True:
    subtotal = yield from accumulate()
    ls.append(subtotal)

특이할만한 점은 서브로 쓰이는 제너레이터는 따로 next()를 사용해서 yield 지점까지 실행시켜둘 필요가 없다는 것이다. 대신에 부모제너레이터의 경우에는 시작시키는 과정이 필요하다.

tallies = []
acc = gather(tallies)
next(acc)

for i in range(4):
  acc.send(i)

## acc로 0,1,2,3 을 보내지만, tallies에는 아직 아무런 변화가 생기지 않는다.
## 아래 코드를 통해서 서브 제너레이터의 실행을 종료시킨다.
acc.send(None)

## 서브 제너레이터가 종료되면 acc의 나머지 코드가 실행되고 tallies가 업데이트된다.
print(tallies)
# [6]

## acc는 새로운 서브제너레이터를 준비중이다.
for i in range(4, 10):
  acc.send(i)
acc.send(None) ## 새 누계값 39가 추가된다.
print(tallies)
# [6, 39]

실질적으로 이런 식으로 코루틴의 위임을 응용할만한 케이스는 아직 딱히 떠오르지 않는다. 대신에 asyncio 에서 쓰이는 await 가 실제로는 코루틴 내에서 다른 코루틴에게 작업 처리를 위임하는 것이며, 실제로 yield from의 동의어가 await 이다.

[Python] 제너레이터와 코루틴

파이썬 제너레이터는 특별한 함수 객체로, yield 구문을 통해서 특정 값을 리턴한 후에도 제거되지 않고 방금 리턴한 그 자리에서부터 이어서 계산을 반복하고 다시 값을 내놓을 수 있다.

파이썬 2.5에서 제너레이터에 특별한 기능이 생겼는데, 바로 제너레이터 속으로 값을 전달하는 기능이다. (PEP342) 이는 매우 흥미로운 패턴으로 이어지게 되는데, 자신의 위치를 기억하고 있다가 다시 그 자리에서 실행이 가능한 제너레이터의 특성상, 두 개의 제너레이터가 번갈아가면서 제어권을 넘기는 형태의 flow를 생각할 수 있다. 이 때 두 제너레이터는 서브루틴과는 다른 개념으로 관계를 맺게 되고 이런 패턴을 코루틴이라 한다.

코루틴은 6~70년대에 기반이 닦여진 기술이나 이후 동시성 작업을 위한 새로운 기술들(스레드 등)이 나타나면서 거의 방치되고 있다가, 스레드가 야기할 수 있는 문제들 (자원경쟁이나, 데드락등의 문제로 인해 디버깅이 매우 까다롭다.) 때문에 새롭게 주목받고 있다.  이는 주 실행 흐름과는 독립적으로 운용가능한 함수가 존재한다는 뜻이며, 따라서 별도의 스레드 없이, 메인 스레드 상에서 번갈아가며 병렬처리와 유사한 동작을 수행할 수 있기 때문이다.

하지만 이런 특수한 상황이 아니더라도 제너레이터 혹은 코루틴은 파이썬 프로그래밍에서 매우 중요한 비중을 차지한다. 많은 경우에 제너레이터는 간단한 클래스를 대체할 수 있으며, 단위 작업을 제너레이터로 만들어서 일련의 작업에 대한 파이프라인을 구성하는 식으로 프로그램을 작성할 수 있다. 이 글에서는 제너레이터와 코루틴을 어떻게 만들고 활용할 수 있는지에 대해 살펴보도록 하겠다.

[Python] 제너레이터와 코루틴 더보기