24
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Python】async/awaitで非同期リクエスト

Posted at

Pythonでの非同期リクエストには、asyncioモジュールを用います。
requestsパッケージを用いて、APIを非同期で叩くコードを作成してみます。

環境

  • Python 3.7.4

API

time.sleep()を用いてもよかったのですが、今回はより実践よりにAPIを取得するパターンを想定してみます。
モックサーバーとして、JSONPlaceholderを使用しました。

JSONPlaceholder

一覧を取得するAPI(以下の6つ)を同期・非同期でリクエストし、それぞれの結果を一つのリストにした結果を返すコードを書きます。

https://jsonplaceholder.typicode.com/posts
https://jsonplaceholder.typicode.com/comments
https://jsonplaceholder.typicode.com/albums
https://jsonplaceholder.typicode.com/photos
https://jsonplaceholder.typicode.com/todos
https://jsonplaceholder.typicode.com/users

同期リクエスト

まずは同期処理でのリクエストを行ってみます。
普段のPythonコード通りに書くと、以下のようになります。

import time
import requests

BASE_URL = "https://jsonplaceholder.typicode.com/"


def calc_time(fn):
    """関数の実行時間を計測するデコレータ"""
    def wrapper(*args, **kwargs):
        start = time.time()
        fn(*args, **kwargs)
        end = time.time()
        print(f"[{fn.__name__}] elapsed time: {end - start}")
        return
    return wrapper


def get_sync(path: str) -> dict:
    print(f"/{path} request")
    res = requests.get(BASE_URL + path)
    print(f"/{path} request done")
    return res.json()


@calc_time
def main_sync():
    data_ls = []
    paths = [
        "posts",
        "comments",
        "albums",
        "photos",
        "todos",
        "users",
    ]
    for path in paths:
        data_ls.append(get_sync(path))
    return data_ls

if __name__ == "__main__":
    main_sync()

実行すると、以下のような出力が得られます。

リクエスト1 -> リクエスト1完了
リクエスト2 -> リクエスト2完了
リクエスト3 -> リクエスト3完了...

と実行されていることがわかります。

/posts request
/posts request done
/comments request
/comments request done
/albums request
/albums request done
/photos request
/photos request done
/todos request
/todos request done
/users request
/users request done
[main_sync] elapsed time: 1.157785415649414

非同期リクエスト

次に非同期リクエストを行います。

非同期で行うタスクはイベントループ内で実行されます。
イベントループを取得するには、asyncio.get_event_loop()を用います。

loop.run_until_completeは、その名前の通りそれぞれのタスクが実行され終わるまでイベントループが実行されるメソッドです。
このメソッドの返り値は、それぞれの非同期実行タスクの返り値が入ったリストです。
実行される順番は保障されていないのですが、返り値は引数に渡した順番で返ってくるので、順番が重要な場合も利用できます。

async defで宣言されるget_asyncはコルーチン関数と呼ばれます。
コルーチン関数内でのawait式は、コルーチン関数の実行を返り値が戻るまで一時停止されます。

import asyncio

# コルーチン関数
async def get_async(path: str) -> dict:
    print(f"/{path} async request")
    url = BASE_URL + path
    loop = asyncio.get_event_loop()
    # イベントループで実行
    res = await loop.run_in_executor(None, requests.get, url)
    print(f"/{path} async request done")
    return res.json()


@calc_time
def main_async():
    # イベントループを取得
    loop = asyncio.get_event_loop()
    # 非同期実行タスクを一つのFutureオブジェクトに
    tasks = asyncio.gather(
        get_async("posts"),
        get_async("comments"),
        get_async("albums"),
        get_async("photos"),
        get_async("todos"),
        get_async("users"),
    )
    # 非同期実行、それぞれが終わるまで
    results = loop.run_until_complete(tasks)
    return results


if __name__ == "__main__":
    main_async()

出力は以下のようになります。

リクエスト1 リクエスト2 リクエスト3...
リクエスト1完了 リクエスト2完了 リクエスト3完了...

と処理されていることがわかります。
また実行時間もかなり短縮されていることがわかります。

/posts async request
/comments async request
/albums async request
/photos async request
/todos async request
/users async request
/users async request done
/todos async request done
/posts async request done
/albums async request done
/comments async request done
/photos async request done
[main_async] elapsed time: 0.17921733856201172
24
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?