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

지난 글에서 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 키워드가 쓰였다면 이는 일반적인 함수가 아니라 제너레이터를 만드는 제너레이터 함수(혹은 코루틴 함수)가 된다. 제너레이터는 next() 함수를 통해서 생성하는 값을 꺼낼 수 있고, 동시에 .send() 메소드를 써서 그 내부로 값을 전달할 수 있다. 특히 이렇게 값을 주입해 줄 수 있는 제너레이터를 코루틴이라 한다고 했다.

코루틴 혹은 제너레이터의 내부에서 다른 코루틴이나 제너레이터의 결과값을 그대로 사용하는 경우가 있을 수 있다. 예를 들어 다음의 경우, 주어진 값으로부터 1씩 내렸다가 다시 0부터 n-1까지 값을 생성하는 제너레이터가 있다고 하자.

def foo(n):
  for i in range(n, 0, -1):
    yield i
  for i in range(n):
    yield i

list(foo(5))
# -> [5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

이렇게 제너레이터 내부에서 다른 제너레이터의 결과를 사용하는 경우에 yield from 으로 코드를 간결하게 만들 수 있다.

def foo(n):
  yield from range(n, 0, -1)
  yield from range(n)

사실 yield from은 실용적인 맥락에서는 별로 쓰일일이 없다. Python 3.5에서 비동기 코루틴의 결과를 기다리는 await 문이 추가되기 전에 같은 기능을 위한 문법으로 추가된 것이기 때문이다.


yield from이 작동하는 방식은 간단하다. 제너레이터/코루틴 내부에서 다른 제너레이터/코루틴에 대해서 yield from subcoroutine()을 사용하는 것이다.

  1. 이 경우 해당 코루틴에 대해 next(co), co.send(v)를 호출하는 것은 곧장 subcoroutine()의 동작으로 연결되며, co 내부의 yield from 지점에서는 코드가 진행되지 않는다.
  2. 서브코루틴이 동작을 완료하여 StopIteration 예외를 일으키면 yield from 구문이 종료되고 다음 라인으로 실행 흐름이 이동한다.

yield from을 사용하여 작업을 위임하는 경우, next()send()가 동일하게 위임된다. 차이가 있다면 next()send(None)과 똑같이 동작하게 된다는 것이다.

다음은 해당 기능을 설명한 PEP380에 등장하는 예제이다. 누계를 받아서 리턴해주는 acc() 코루틴을 사용해서 여러 수열의 누적합을 만드는 예이다. 유용할지는 모르지만, 작업 위임이 어떤식으로 동작하는지는 볼 수 있는 예이다.

def acc():
    s = 0
    while True:
        n = yield s
        if n is None:
            return s
        s += n
        

@coroutine
def gather(xs):
    while True:
        subtotal = yield from acc()
        xs.append(subtotal)

ns = []
g = gather(ns)
g.sender(10) # -> 10
g.sender(20) # -> 30
g.sender(30) # -> 60
next(g)
# ns = [60]

파이썬의 제너레이터와 코루틴

파이썬 제너레이터는 특별한 종류의 함수 객체이다. 함수 내부에서 yield 구문을 사용하여 특정 값을 내놓은 후에도 실행을 종료하지 않아 제거되지 않고, 다시 그 자리에서부터 이어서 계산을 반복하고 다시 값을 내놓을 수 있다. rangemap, filter 등의 객체가 제너레이터의 일종이라고 할 수 있다.

아주 먼 옛날, 파이썬 2.5에서 제너레이터에 특별한 기능이 생겼는데, 바로 제너레이터 속으로 값을 전달하는 기능이다. (PEP342) 이는 매우 흥미로운 패턴으로 이어지게 되는데, 실행 중 한 번 yield 문을 만나 자신의 위치를 기억하고 있다가 다시 그 자리에서 실행이 가능하다는 점에서 두 개 이상의 제너레이터가 서로 값을 주고 받으면서 교차식으로 실행하는 것이 가능하다. 이는 일반적인 함수 호출의 패턴인 주 루틴 – 서브 루틴의 관계와 달리 두 개의 루틴이 함께 실행된다는 부분에서 코루틴(coroutine)이라고 부른다.

코루틴은 사실 완전히 새롭게 등장한 개념은 아니었다. 이미 6~70년대에 기반이 닦여진 기술이이었다. 당시에는 작업 흐름의 분산을 위한 여러 가지 개념들이 도입되고 시도되고 있었는데, 이 당시에 이러한 기술들 중에서 가장 환호를 받았던 것은 다름 아닌 멀티스레드였다.

멀티스레드가 큰 인기를 얻고 발전해 나가면서 상대적으로 코루틴은 거의 방치되다 시피하였으나, 규모가 커짐에 따라 멀티 스레드는 자원 경쟁이라든지 동기화문제 등 더 큰 골칫거리를 가져왔다. 이러한 문제로 인해 코루틴 개념은 그린릿(greenlet)이나 경량스레드(lightweight thread)라는 이름으로 재발견되어 주목받는 경우도 있다.

코루틴이 흥미로운 지점은 멀티스레드 없이 하나의 스레드 위에 여러 개의 실행흐름이 존재할 수 있다는 것이다. 즉 멀티스레드에서의 골치 아픈 문제들을 끌어들이지 않고서도 실행 흐름을 분산할 수 있다는 것을 의미한다.

하지만 분산처리와 같은 상황이 아니더라도 제너레이터 혹은 코루틴은 파이썬 프로그래밍에서 매우 중요한 비중을 차지한다. 멀티스레드와 같은 적극적인 동시성이 아니더라도, 제너레이터의 ‘느긋한(lazy)’한 특성은 실제로는 concurrent 하지 않은 작업들을 마치 동시에 진행되는 것처럼 다룰 수 있게 하며, 무거운 연산을 가능한 뒤로 미루어 실행 시간내의 체감 퍼포먼스가 좋은 것처럼 보일 수 있게 한다. (사실 이정도면 충분한 것이, 파이썬의 멀티스레드는 실제로는 동시에 실행되지 않기 때문이다.)

이 외에도 제너레이터와 코루틴은 간단한 클래스를 대체할 수 있으며, 일련의 처리 과정에서의 단위 작업을 구성하고 이들을 선언적으로 연결하는 방법으로도 활용할 수 있다. 오늘 이 글에서는 제너레이터와 코루틴에 대해 알아보도록 하자.

파이썬의 제너레이터와 코루틴 더보기