POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Abu Ashraf Masnun

本記事は、原著者の許諾のもとに翻訳・掲載しております。

(訳注: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()