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

 
0
このエントリーをはてなブックマークに追加
Daichi Takayama
Daichi Takayama (高山 大地)

以下の文章は執筆者のAbu Ashraf Masnunさんからの許可を得て翻訳したものです。

https://masnun.com/2015/11/13/python-generators-coroutines-native-coroutines-and-async-await.html

注意

こちらの記事は、主にPython 3.4で導入された機能について説明しており、ネイティブコルーチンとasync/await構文はPython 3.5で導入されました。コードを試す場合はPython 3.5を使用することをお勧めします。Pythonの更新方法がわからない場合は、こちらをご参照ください。


ジェネレーター

ジェネレーターとは、値を生成する関数です。通常の関数は値を返すとそのスコープが破棄されます。再び呼び出すと、関数が最初から実行されます。つまり、一度きりの実行です。しかし、ジェネレーター関数は値を生成(yield)して関数の実行を一時停止することができます。制御は呼び出し元のスコープに戻ります。それにより、次に呼ばれた時には、停止したところから再開して新たな値を提供することが可能です。以下の例を見てみましょう:

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

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

ジェネレーター関数からは直接値が返されていないことに注意してください。代わりにiterableなジェネレーターオブジェクトを取得します。このオブジェクトに対してnext()メソッドを使うことで、値を一つずつ取り出したり、forループで繰り返し処理を行うことができます。

ジェネレーターの便利さは、大量のデータを扱う際に特に顕著です。例えば上司から100までの数字のシーケンスを生成する関数を書くように言われたとします(range()の超シンプルなバージョンです)。書きました。空のリストを取り、数字も追加していって、その数字のリストを返しました。しかし、そこで要件が変わり、1,000万までの数字を生成する必要が出てきました。もし1,000万までの数を生成するとなると、リストに保存することでメモリを圧迫してしまいます。ここでジェネレーターが役立ちます。リストに保存することなく、必要に応じて数を次々と生成できるのです。例えば以下のように:

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を超えた後は実行しませんでしたが、コンソールで試した場合、一つ一つの数字をどのように生成し続けているかがわかるかと思います。関数のコンテキストに入ったり出たりしながら、実行を一時停止して再開することによって行われているのです。

まとめ:ジェネレーターは、実行を一時停止し、複数の値を順に生成することができる関数です。これにより、イテレート可能なオブジェクトを得ることができ、大量のデータをメモリ効率良く扱うことが可能になります。


コルーチン

前のセクションでは、ジェネレーターがどのようにして関数から値を「取り出す」ために一時停止するかを見ました。しかし、関数に値を「送り込む」場合はどうでしょう?ここでコルーチンの登場です。値を取り出すために使っていたyieldキーワードは、関数内で式("="の右側)としても機能します。ジェネレーターオブジェクトのsend()メソッドを使い、関数に値を戻すことができます。これを「ジェネレーターベースのコルーチン」と呼びます。以下に例を示します:

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

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

ここで何が起きているのでしょうか?最初は通常通りnext()関数を使って値を取得しています。これによりyield "Hello"に到達し、"Hello"が得られます。次にsend()メソッドを使って値を送信します。これにより関数が再開され、送信した値がhelloに割り当てられ、次の行に進んで文が実行されます。結果としてメソッドの戻り値として"World"が得られます。

ジェネレーターベースのコルーチンは、しばしばジェネレーターとコルーチンの両方の意味で使われるため、両者はしばしば同義として扱われますが、完全に同一ではありません。Python 3.5以降では、async/awaitキーワードを使用したネイティブコルーチンが導入され、非同期プログラミングをより直接的にサポートするようになりました。この新しい概念については、後ほど詳しく見ていきましょう。


Async I/Oとasyncioモジュール

Python 3.4から導入されたasyncioモジュールは、一般的な非同期プログラミングを手軽に実現するためのAPIを提供しています。このモジュールはコルーチンと組み合わせて利用し、非同期I/O操作を簡素化します。公式ドキュメントにある例を参考にしましょう:

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)を作成し、継続的に現在時刻を出力します。そして、asyncio.sleep()関数呼び出しからの結果を待つためにyield fromキーワードを使用します。この関数は指定された秒数後に完了するコルーチンです。ランダムな秒数を渡しています。その後、asyncio.ensure_future()を使用して、デフォルトイベントループでのコルーチンの実行をスケジュールします。そして、ループに対して無限に実行を続けるように依頼します。

出力を見ると、2つのコルーチンが並行して実行されているのを確認できます。yield fromを使用すると、イベントループはしばらくの間忙しくなることを知っているため、コルーチンの実行を一時停止し、もう片方のコルーチンを実行します。このようにして2つのコルーチンが並行して実行されますが、イベントループがシングルスレッドであるため、並列ではありません。

ちなみに、yield fromfor 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()

上記のコードにおける重要な点に注目してください。ネイティブコルーチンを定義するには、defキーワードの前にasyncキーワードを使用しなければなりません。ネイティブコルーチンの内部では、yield fromの代わりにawaitキーワードを使用します。

この新しい構文によって、非同期プログラミングがより簡潔に、直感的に書けるようになりました。asyncで始まる関数は、非同期に実行されることを意味し、awaitはその関数が完了するまで待機するために用います。これによって、複数の非同期タスクがバックグラウンドで実行されている間に、現在のタスクが中断されずに進行することを可能にします。

ネイティブコルーチンを活用することで、非同期I/O、ネットワークリクエスト、その他の待ち時間が発生する操作を、イベントループを介して効率良く処理できます。リアルタイムのデータ処理や高性能なウェブサーバー、非同期APIなど、現代のアプリケーション開発において欠かせない技術となっています。


ネイティブ対ジェネレーターベースのコルーチン:相互運用性

ネイティブコルーチンとジェネレーターベースのコルーチンの間には、構文の違いを除いて機能的な差はありません。しかし、これらの構文を混在させることは許されていません。つまり、ジェネレーターベースのコルーチン内で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()

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。