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

지난 글에서 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.