Pythonのジェネレータ、コルーチン、ネイティブコルーチン、そしてasync/await

(訳注:2016/3/9、いただいたフィードバックを元に記事を修正いたしました。)

注意: この記事で書かれている機能は、大部分がPython 3.4で導入されたものです。ネイティブコルーチンとasync/await構文はPython 3.5でサポートされました。そのため、本記事に記載されているコードを試す場合はPython 3.5の利用をお勧めします。

ジェネレータ

ジェネレータは値を生成する関数です。普通、関数はreturnで値を返したあと、その下層のスコープは破棄します。関数を再度呼び出す場合、その関数はゼロから起動されることになります。つまり1回限りの実行となります。しかしジェネレータ関数は値をyieldで返し、関数の実行を一時停止します。その後、関数を呼び出したスコープにコントロールが移ります。関数を再び呼び出して次の値を(存在すれば)得たい時は、実行を再開することができます。では、以下の例をご覧ください。

def simple_gen():
    yield "Hello"
    yield "World"


gen = simple_gen()
print(next(gen))
print(next(gen))

注意していただきたいのは、ジェネレータ関数が値を直接返すことはないということです。ジェネレータ関数を呼び出すと、反復処理が可能なジェネレータオブジェクトが返されます。そのため、ジェネレータオブジェクトでnext()を呼び出し、値を繰り返し生成することが可能です。または、forループを実行することもできます。

では、どうすればジェネレータを便利に使えるのでしょうか? 例として、上司から100までの連続した数字を生成する関数を書くように言われたとしましょう。(range()をものすごくシンプルにしたバージョンのようなものです)。あなたが書いてみたコードは、空のリストを用意し、そこに数字を順に入れ、数字とともにそのリストを返すというものでした。しかしそこで要求事項が変更され、1000万までの連続した数字を生成する必要が出てきたとしましょう。もし1000万個の数字をリストに格納しようとすれば、すぐにメモリ不足に陥ってしまいますね。そのような状況で役立つのがジェネレータ関数です。リストに数字を格納しなくても、数字を生成できます。コードは以下のようになります。

def generate_nums():
    num = 0
    while True:
        yield num
        num = num + 1


nums = generate_nums()

for x in nums:
    print(x)

    if x > 9:
        break

ここでは、あえて数字が9に達したら終了するように作っていますが、コンソールで試してみれば、どのように次から次へと数字が生成されるのかを見られるでしょう。関数のコンテキストを行ったり来たりすることで、一時停止とレジュームを繰り返しているのです。

まとめ: ジェネレータ関数は一時停止が可能で、1つの値だけを返すのではなく複数の値を生成することができる関数です。呼び出されると、反復処理が可能なジェネレータオブジェクトを返します。この反復処理で(繰り返しで)、値を1つずつ得ることが可能です。

コルーチン

前のセクションで、ジェネレータを使うと関数のコンテキストからデータをプルできる、つまり引き出せること(また、実行を一時停止できること)を説明しました。では、データをプッシュしたい場合はどうでしょうか。コルーチンが利用できます。値をプルするために使ったyieldというキーワードは、関数内部の表現(”=”の右側)としても使うことができます。また、関数に値を返すために、ジェネレータオブジェクト上でsend()メソッドを使うこともできます。これは”ジェネレータベースのコルーチン”と呼ばれています。例をご紹介しましょう。

def coro():
    hello = yield "Hello"
    yield hello


c = coro()
print(next(c))
print(c.send("World"))

ここでは何が起こっているのでしょうか。まずはいつも通り、next()関数を使って値を得ようとします。すると、yield "Hello"へ来て、”Hello”を得ることができます。次にsend()メソッドを使って値を送ります。これで関数が再開されます。helloに送った値を指定し、次の行まで進んで命令を実行します。そうして、send()メソッドの戻り値として”World”が得られます。

ジェネレータベースのコルーチンを使う際、”ジェネレータ”と”コルーチン”は同じものとして扱う場合が多いです。完全に同じものではないのですが、交換可能な場合が非常に多いのです。しかし、Python 3.5では、ネイティブコルーチンと共にasync/awaitというキーワードがあります。これについては、この投稿内で後ほど説明しましょう。

非同期I/Oとasyncioモジュール

Python 3.4から新たにasyncioモジュールが加わりました。これは、一般的な非同期プログラミングで役立つ、素晴らしいAPIを提供しています。コルーチンをasyncioモジュールと一緒に使うことで非同期入出力を容易に行えるようになります。公式ドキュメントから例をご紹介しましょう。

import asyncio
import datetime
import random


@asyncio.coroutine
def display_date(num, loop):
    end_time = loop.time() + 50.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(random.randint(0, 5))


loop = asyncio.get_event_loop()

asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))

loop.run_forever()

とても分かりやすいコードになっています。コルーチンdisplay_date(num, loop)を作ります。これは識別子(数字)とイベントループを使い、現在時刻を表示し続けます。そして、yield fromキーワードを使って、asyncio.sleep()関数の呼び出し結果を待ちます。この関数は、与えられた秒数が経過した後に完了するコルーチンです。そのため、ランダムな秒数を与えます。そして、デフォルトのイベントループ内にあるコルーチンの実行スケジュールを決めるために、asyncio.ensure_future()を使います。それからループし続けるよう設定します。

出力を見ると2つのコルーチンが実行されているのが分かります。yield fromを使うと、しばらく忙しくなると分かるので、イベントループはコルーチンの実行を一時停止し、別のコルーチンを実行させます。そのため2つのコルーチンが実行されるのです(イベントループは単一のスレッドなので並行して実行はされません)。

yield fromは、for x in asyncio.sleep(random.randint(0, 5)): yield xの素晴らしい糖衣構文であり、非同期のコードをより明瞭にしてくれます。

ネイティブコルーチンとasync/await

ところで、これはまだジェネレータベースのコルーチンを使っています。Python 3.5では、新たにasync/await構文を使うネイティブコルーチンが加わりました。以前の関数は以下のように書くことができます。

import asyncio
import datetime
import random
 
 
async def display_date(num, loop, ):
    end_time = loop.time() + 50.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(random.randint(0, 5))
 
 
loop = asyncio.get_event_loop()
 
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
 
loop.run_forever()

6行目と12行目をご覧ください。ネイティブコルーチンは、defキーワードの前のasyncキーワードで定義しなければいけません。ネイティブコルーチン内部では、yield fromの代わりにawaitキーワードを使います。

ネイティブコルーチンとジェネレータベースのコルーチンの比較:インターオペラビリティ(相互運用性)

ネイティブコルーチンとジェネレータベースのコルーチンの間で機能的な違いはありませんが、構文だけ異なります。そのため構文を混ぜて使うことはできません。つまりawaitをジェネレータベースのコルーチンの内部で使ったり、yieldyield fromをネイティブコルーチンの内部で使ったりすることはできないのです。

このような違いがあるにも関わらず、この両者を相互運用することができます。従来のジェネレータベースのコルーチンに@types.coroutineというデコレータを加えるだけでいいのです。これで、どちらかのコルーチンを、別のコルーチンの内部で使えるようになります。つまり、awaitをネイティブコルーチンの内部にあるジェネレータベースのコルーチンから使ったり、yield fromをジェネレータベースのコルーチン内部のネイティブコルーチンで使ったりすることができます。例をお見せしましょう。

import asyncio
import datetime
import random
import types


@types.coroutine
def my_sleep_func():
    yield from asyncio.sleep(random.randint(0, 5))


async def display_date(num, loop, ):
    end_time = loop.time() + 50.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await my_sleep_func()


loop = asyncio.get_event_loop()

asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))

loop.run_forever()