【Python】aiohttpを使ってみた

# はじめに

asyncio ライブラリはリクエストを非同期処理で行うため、リクエスト中にブロッキングが発生しません。一方で requests ライブラリはリクエストを同期処理で行うため、リクエストが完了するまで次の処理に進むことができません。
この記事では以下について書きます。

- aiohttp を使ってみる
- requests と aiohttp の動作の違いを確認する
- 考察

バージョン：

```
Python: 3.10.12
aiohttp: 3.12.4
requests: 2.32.3
```

# aiohttp を使ってみる

まずライブラリをインストールします。

```bash
pip install aiohttp
```

PyPI：

https://pypi.org/project/aiohttp/

公式ドキュメント：

https://docs.aiohttp.org/en/stable/

インストール後、以下のファイルを作成し、実行します。

```python
import asyncio

import aiohttp

async def req_aiohttp(name):
    print(f"{name} のリクエストが開始しました。")

    async with aiohttp.ClientSession() as session:
        async with session.get("https://httpbin.org") as response:
            print("Status:", response.status)
            print("Content-type:", response.headers["content-type"])

            html = await response.text()
            print("Body:", html[:15], "...")

    print(f"{name} のリクエストが完了しました。")


async def main():
    await req_aiohttp("Alice")

if __name__ == "__main__":
    asyncio.run(main())
```

実行結果：

```
------ Alice のリクエストが開始しました。------
Status: 200
Content-type: text/html; charset=utf-8
Body: <!DOCTYPE html> ...
------ Alice のリクエストが完了しました。------
```

# requsts と aiohttp の動作の違いを確認する

並列で2人のユーザーがリクエストを実行してみます。3 秒かかるリクエストを実行します。

以下のサイトはリクエスト送信からレスポンス受信まで 3 秒かかります。

https://httpbin.org/delay/3

## aiohttp

検証コード：

```python
import asyncio
from datetime import datetime

import aiohttp

async def req_aiohttp(name):
    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")

    async with aiohttp.ClientSession() as session:
        async with session.get("https://httpbin.org/delay/3") as response:
            print(f"{name} Status: {response.status}")

    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")

async def main():
    users = ["Alice", "Bob"]

    tasks = [asyncio.create_task(req_aiohttp(user)) for user in users]

    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())
```

実行結果：

```
------ 00:28:44.004863 - Alice のリクエストが開始しました。 ------


------ 00:28:44.005018 - Bob のリクエストが開始しました。 ------

Bob Status: 200
Alice Status: 200

------ 00:28:48.030639 - Alice のリクエストが完了しました。 ------


------ 00:28:48.031345 - Bob のリクエストが完了しました。 ------
```

Alice、Bob のリクエストは同時に開始・完了しています。

## requests

検証コード：

```python
import asyncio
from datetime import datetime

import requests

async def req_requests(name):
    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")

    with requests.Session() as session:
        with session.get("https://httpbin.org/delay/3") as response:
            print(f"{name} Status: {response.status_code}")

    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが完了しました。 ------\n")

async def main():
    users = ["Alice", "Bob"]

    tasks = [asyncio.create_task(req_requests(user)) for user in users]

    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())
```

実行結果：

```
------ 01:14:16.735965 - Alice のリクエストが開始しました。 ------

Alice Status: 200

------ 01:14:20.595169 - Alice のリクエストが完了しました。 ------


------ 01:14:20.596051 - Bob のリクエストが開始しました。 ------

Bob Status: 200

------ 01:14:24.445815 - Bob のリクエストが完了しました。 ------
```

Alice のリクエスト完了後、Bob のリクエストが開始しています。

## 検証結果まとめ

以下のことが確認できました。

```
同期処理(requests):
[ Alice 開始 ] ----3秒---- [ Alice 終了 ] -> [ Bob 開始 ] ----3秒---- [ Bob 終了 ]

非同期処理(aiohttp):
[ Alice 開始 ] [ Bob 開始 ]
----3秒----
[ Alice 終了 ] [ Bob 終了 ]
```

# 考察

## requests ではなぜブロッキングが発生するか

 requests のコードでは、`async with` を使っていません。`async with` で実行すると以下のエラーが発生します。requests は同期処理のため使いたくても使えません。

 ```
 ------ 00:43:52.276705 - Alice のリクエストが開始しました。 ------


------ 00:43:52.276879 - Bob のリクエストが開始しました。 ------

Traceback (most recent call last):
  File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 27, in <module>
    asyncio.run(main())
  File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 24, in main
    await asyncio.gather(*tasks)
  File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 9, in req_requests
    async with requests.Session() as session:
AttributeError: __aenter__
 ```

 そのため、requests のコードでは以下と同じ状態になっています。

 ```python
import asyncio
import time

async def wait_function(name):
    print(f"------ {name} は 3 秒待機します。 ------")
    # ここでブロッキング
    time.sleep(3)
    print(f"------ {name} の待機が終わりました。 ------")

async def main():
    users = ["Alice", "Bob"]

    tasks = [asyncio.create_task(wait_function(user)) for user in users]

    await asyncio.gather(*tasks)

if __name__ == '__main__':
    asyncio.run(main())
 ```

 実行結果：

 ```
------ Alice は 3 秒待機します。 ------
------ Alice の待機が終わりました。 ------
------ Bob は 3 秒待機します。 ------
------ Bob の待機が終わりました。 ------
 ```

 並列処理を行うためには以下のように `await` を使う必要があります。

 ```python
import asyncio
import time

async def wait_function(name):
    print(f"------ {name} は 3 秒待機します。 ------")
    # ブロッキングは発生しない
    await asyncio.sleep(3)
    print(f"------ {name} の待機が終わりました。 ------")

async def main():
    users = ["Alice", "Bob"]

    tasks = [asyncio.create_task(wait_function(user)) for user in users]

    await asyncio.gather(*tasks)

if __name__ == '__main__':
    asyncio.run(main())
 ```

 実行結果：

 ```
 ------ Alice は 3 秒待機します。 ------
------ Bob は 3 秒待機します。 ------
------ Alice の待機が終わりました。 ------
------ Bob の待機が終わりました。 ------
 ```

## 非同期関数内で同期リクエストを並列処理する

イベントループを使用します。以下の記事を参考にしました。

https://qiita.com/nilwurtz/items/85b81febefd3f3f2899a

公式ドキュメント：

https://docs.python.org/ja/3.13/library/asyncio-eventloop.html

検証コード：

```python
import asyncio
from datetime import datetime

import requests

def sync_request(name):
    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")

    with requests.Session() as session:
        with session.get("https://httpbin.org/delay/3") as response:
            print("Status:", response.status_code)

    print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが完了しました。 ------\n")

async def req_requests(name):
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, sync_request, name)

async def main():
    users = ["Alice", "Bob"]

    tasks = [req_requests(user) for user in users]

    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())
```

実行結果：

```
------ 01:15:44.411194 - Alice のリクエストが開始しました。 ------


------ 01:15:44.411489 - Bob のリクエストが開始しました。 ------

Alice Status: 200

------ 01:15:48.260434 - Alice のリクエストが完了しました。 ------

Bob Status: 200

------ 01:15:48.261894 - Bob のリクエストが完了しました。 ------
```

## 疑問点

aiohttp では以下のように完了メッセージが並んで表示されていました。

```
Bob Status: 200
Alice Status: 200

------ 00:28:48.030639 - Alice のリクエストが完了しました。 ------


------ 00:28:48.031345 - Bob のリクエストが完了しました。 ------
```

しかし `run_in_executor` の場合、以下のように完了メッセージが並んでいません。

```
Alice Status: 200

------ 01:15:48.260434 - Alice のリクエストが完了しました。 ------

Bob Status: 200

------ 01:15:48.261894 - Bob のリクエストが完了しました。 ------
```

この理由がわかっていません。

# まとめ

- aiohttp はリクエストを非同期処理で行うため、リクエスト中にブロッキングが発生しない
- requests はリクエストを同期処理で行うため、並列処理を行うにはイベントループが必要

# 参考

https://pypi.org/project/aiohttp/

https://docs.aiohttp.org/en/stable/

https://qiita.com/nilwurtz/items/85b81febefd3f3f2899a

https://docs.python.org/ja/3.13/library/asyncio-eventloop.html