Python: async with の使い方

More than 1 year has passed since last update.

python 3.5 から使える async with の使い方です。

PEP 492 -- Coroutines with async and await syntax


async with?

コンテキストマネージャの async 版です。


  1. DB にコネクションを張る <- 時間がかかる = async

  2. 張ったコネクションでなにかする

  3. コネクションを閉じる

みたいな処理があるときに

async with MyDB() as db:

db.do_something()

というコードを実効するとして MyDB の方をうまく作っておけば async with ブロックを抜けたときに自動で db のコネクションを閉じるとかができます。


基本的な使い方

例えば、 aiomysql をラップしたクラスを作って、使ってみましょう。


my_db.py


my_db.py

import asyncio

import aiomysql

class MyDB:
# async with に入る直前に呼ばれます。
async def __aenter__(self):
loop = asyncio.get_event_loop()
self._connection = await aiomysql.connect(
host='localhost',
port=3306,
user='root',
password='ultrastrongpassword',
db='my_db',
charset='utf8',
loop=loop
)
return self

# async with ブロックを抜けた直後に呼ばれます。
async def __aexit__(self, exc_type, exc, tb):
self._connection.close()

async def fetchall(self, query, args=[]):
cursor = await self._connection.cursor()
await cursor.execute(query, args)
return await cursor.fetchall()


aiomysql の接続先とかを隠蔽した感じのものです。

大事なのは __aenter____aexit__ です。

これらを実装することで、このクラスは非同期のコンテキストマネージャとして使えるようになります。

呼ばれるタイミングはコメントのとおりです。

例のように、 aenter でリソースのセットアップ、 aexit でリソースの開放をするのがよくあるパターンでしょう。


使う側

呼び出し側も見てみましょう。


使用する側

from my_db import MyDB

class Hoge:
async def call_db(self):
# db = MyDB()
# async with db:
# でも同じ意味だよ。
async with MyDB() as db:
result = db.fetchall('select * from some_table')
print(result)


async with [非同期のコンテキストマネージャインスタンス] as [__aenter__ の返り値]: の書式になります。

これを実行すると、以下のような順番で処理が進みます。


  1. MyDB のインスタンスが作られる(__init__ が呼ばれる)

  2. Hoge の async with 節の処理が始まる

  3. MyDB の __aenter__ が呼ばれる

  4. MyDB の __aenter__ の返り値が async with as db の db に格納される

  5. Hoge の result = db.fetchall('select * from some_table') が実行される

  6. MyDB の __aexit__ が呼ばれる

async with の中にいるときだけ db インスタンスはもろもろのリソースを使えるようになる、という感じですね。


その他の使い方いろいろ


ファクトリで使いたい

async with MyDB.connect() as db: みたいにしたいとき


使用する側

class Hoge:

async def call_db(self):
async with MyDB.connect() as db:
result = db.fetchall('select * from some_table')
print(result)

以下のように普通に connect メソッドを実装するだけです。


my_db.py

class MyDB:

__slots__ = ['_connection']

async def __aenter__(self):
()

async def __aexit__(self, exc_type, exc, tb):
self._connection.close()

async def fetchall(self, query, args=[]):
()

@classmethod
def connect(cls):
return cls()


async with の後に MyDB (非同期コンテキストマネージャ)のインスタンスさえあれば良いので。


自分自身のインスタンスを返すんじゃない系で async with したい


使用する側

class Hoge:

async def call_db(self):
async with MyDB.direct_fetch('select * from some_table') as rows:
print(rows)

この場合 async with の中身の MyDB.direct_fetch の返り値は MyDB インスタンスでは無さそうなので、ちょっと考える必要があります。

aiohttp の get とか が参考になるかもしれません。

以下のようにプライベートなコンテキストマネージャクラスを作ることで対応できます。


my_db.py

class _QueryContextManager:

def __init__(self, coro):
self._coro = coro

async def __aenter__(self):
loop = asyncio.get_event_loop()
self._connection = await aiomysql.connect(
host='localhost',
port=3306,
user='root',
password='ultrastrongpassword',
db='my_db',
charset='utf8',
loop=loop
)
return await self._coro(self._connection)

async def __aexit__(self, exc_type, exc, tb):
self._connection.close()

class MyDB:
__slots__ = ['_connection']

@classmethod
def direct_fetch(cls, query, args=[]):
async def fetch(connection):
cursor = await connection.cursor()
await cursor.execute(query, args)
return await cursor.fetchall()

# プライベートなコンテキストマネージャを返す。
return _QueryContextManager(coro=fetch)


うーん。無理矢理感は否めませんね。。

もっと良い書き方はあるかもしれません。

aiohttp では request の返り値のインスタンスからコネクションを閉じれる(self._resp.release())のでもう少しきれいになっているようです。