Python Asyncio入門

**Asyncio**は、大雑把に言えば、PythonでNodeのような非同期プログラムを行えるようにするモジュールです。Pythonでは、スレッド（**concurrent.futures**）を用いて並行プログラムを書くことができますが、Asyncioではもう少し軽量の並行プログラムを実現できます。

この辺は「Fluent Python」に詳しいですが、いかんせんAsyncioの箇所は最新のPythonのバージョンで大きく変更されています。以下の公式サイトが貴重な情報源になります。

[Asyncio公式サイト](https://docs.python.org/ja/3/library/asyncio.html)


ここでは、最も有名なAsyncioライブラリである**aiohttp**を使ったソースコードを説明することで、Asyncioの概念を見ていきたいと思います。

[aiohttp公式サイト](https://docs.aiohttp.org/en/stable/)


以下の関連記事を投稿しました
(追加 2021/02/13) [Django 3.1のAsync Views - Qiita](https://qiita.com/sand/items/96cc70b6fe6bb6472476)
(追加 2022/09/05) [Python Asyncio で作る Socket Server](https://qiita.com/sand/items/4f27e62c7491b8e85d12)

# １． ソースコード

以下のプログラムは、3つのサイト（どれもアマゾンのランキングページです）を並行的にスクレイピングするものです。このプログラムの注意点は、**BeautifulSoup**を使ってhtmlを解析しているので、アマゾンの方でページのHTMLを変えられたら、プログラムも変更が必要となります。


```python:aiohttp_client.py
import asyncio
import aiohttp
import datetime
from bs4 import BeautifulSoup

async def main():
    async with aiohttp.ClientSession() as session:
        sites = [("https://www.amazon.co.jp/gp/top-sellers/books/ref=crw_ratp_ts_books", '本の売れ筋ランキング'),
                 ("https://www.amazon.co.jp/gp/bestsellers/books/466298/ref=zg_bs_nav_b_1_b", 'コンピュータ・ITの売れ筋ランキング' ),
                 ("https://www.amazon.co.jp/gp/bestsellers/books/492352/ref=zg_bs_nav_b_2_466298", 'プログラミングの売れ筋ランキング')]
        tasks = [asyncio.create_task(one_site(session, *site)) for site in sites]
        # tasks = [one_site(session, *site) for site in sites]
        outs = await asyncio.gather(*tasks)
        for out in outs:
            print(out)

async def one_site(session, url, title):
        today = datetime.date.today().strftime('%Y/%m/%d')
        out = "-"*15 + f'{title} ({today})' + "-"*15 + "\n"
        async with session.get(url) as resp:
            data = await resp.text()
            soup = BeautifulSoup(data, "lxml")
            for i, el in enumerate(soup.find_all("li", class_="zg-item-immersion")):
                name  = el.find_all("div", class_="p13n-sc-truncate")[0].string.strip()
                price = el.find("span", class_="p13n-sc-price") # .string.strip()
                price = price.string.strip() if price is not None else "???"
                out = out + f"[{i+1}] {name} ({price})\n"
        return(out)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```

以下が実行例となります。


```
---------------本の売れ筋ランキング (2021/02/01)---------------
[1] ももクロゲッタマン体操 パワー炸裂! 体幹ダイエット DVD67分付き (￥1,760)
[2] 呪術廻戦 1-13巻 新品セット (￥6,292)
[3] くびれ母ちゃんの骨からボディメイク - 3DX BODY - (美人開花シリーズ) (￥1,650)
[4] 【Amazon.co.jp 限定】TVガイドVOICE STARS vol.17 Amazon限定表紙版 (￥1,430)
[5] スマホ脳 (新潮新書) (￥1,078)
[6] デザインのひきだし42 (￥4,200)
[7] オードリー・タン デジタルとAIの未来を語る (￥1,980)
[8] 推し、燃ゆ (￥1,540)
[9] 1日1話、読めば心が熱くなる365人の仕事の教科書 (￥2,585)
[10] 【Amazon.co.jp 限定】味わいリッチな焼き菓子レシピ(特典:米粉のクッキー、アップルパイマフィンレシピ データ配信) (￥1,540)
---
[50] ダービースタリオン 公式全書 (￥2,420)

---------------コンピュータ・ITの売れ筋ランキング (2021/02/01)---------------
[1] iPadはかどる! 仕事技2021(全iPad・iPadOS 14対応/リモートワークにも最適な仕事技が満載) (￥1,320)
[2] スマホ脳 (新潮新書) (￥1,078)
[3] オードリー・タン デジタルとAIの未来を語る (￥1,980)
[4] できる イラストで学ぶ 入社1年目からのExcel VBA できる イラストで学ぶシリーズ (￥1,762)
[5] いちばんやさしいアジャイル開発の教本　人気講師が教えるDXを支える開発手法 「いちばんやさしい教本」シリーズ (￥1,584)
[6] プログラミング超初心者が初心者になるためのPython入門（１）　セットアップ・文字列・数値編 (￥250)
[7] 一生使えるプレゼン上手の資料作成入門 一生使えるシリーズ (￥1,584)
[8] 2040年の未来予測 (￥1,870)
[9] iPad仕事術！SPECIAL 2020（手書きノート大特集! !） (￥990)
[10] 起業の天才!: 江副浩正 8兆円企業リクルートをつくった男 (￥2,200)
---
[50] ヒョーゴノスケ流　イラストの描き方 (￥2,178)

---------------プログラミングの売れ筋ランキング (2021/02/01)---------------
[1] プログラミング超初心者が初心者になるためのPython入門（１）　セットアップ・文字列・数値編 (￥250)
[2] 【Amazon.co.jp 限定】 1冊ですべて身につくHTML & CSSとWebデザイン入門講座 (DL特典: CSS Flexbox チートシート) (￥2,486)
[3] まいぜんシスターズとマイクラを遊ぼう! (扶桑社ムック) (￥1,100)
[4] C#を勉強する順番: 文法プログラマーを卒業する方法 (￥99)
[5] リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice) (￥2,640)
[6] 【Swift】作って学ぼうiOSアプリ開発 (￥1,035)
[7] ゼロからFlaskがよくわかる本: Pythonで作るWebアプリケーション開発入門 (￥630)
[8] スッキリわかるJava入門 第3版 (スッキリシリーズ) (￥2,860)
[9] 実践Data Scienceシリーズ データ分析のためのデータ可視化入門 (KS情報科学専門書) (￥3,520)
[10] 独学プログラマー Python言語の基本から仕事のやり方まで (￥2,420)
---
[50] 基礎から学ぶ Ruby on Rails: １週間の短期間講座！楽しく学ぶRailsの新しい入門書 (￥650)
```



# ２． 解説

以下、この例をasyncioモジュールの側面から見ていきます。

## ２－１．コルーチン
**async def**で宣言された関数のようなものは**コルーチン関数**と呼ばれ、callされると**コルーチン・オブジェクト**を返します。以下コルーチン・オブジェクトを単に **コルーチン** と呼びます。

```python
>>> async def f():
...     return 1
... 
>>> coro = f()
>>> type(coro)
<class 'coroutine'>
>>> coro.send(None) # コルーチン開始
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 1  # return 1
>>>
```

通常のやり方ではないですが手動でコルーチンを動かしてみます。メソッド **send(None)** でコルーチンを開始してみましょう。コルーチンは実行が終了したら**StopIteration**例外を上げます。コルーチン関数が return 1 しているので、StopIteration例外の値は 1 となります。


通常のプログラムにおいては、コルーチンは以下のように**await**で実行され、その戻り値としてコルーチンの実行結果を受け取ることができます。但し、大元のmain()のコルーチンは **asyncio.run()** で実行します。

```python
>>> import asyncio
>>> async def main():
...     result = await f()
...     print(result)
... 
>>> asyncio.run(main())
1
```

aiohttp_client.pyにおいては、aiohttpモジュールの公式サイトにならって、asyncio.run()は使っていません。**loop.run_until_complete()** で実行しています。asyncio.run()では内部的にいろいろなことを行っており、aiohttpモジュールと相性が悪いようです。ちなみにloop.run_until_complete()もコルーチンを実行する関数です。asyncio.run()もloop.run_until_complete()を内部で利用していますが、それ以外に未完のタスクのキャンセルやloop.close()処理など多くの仕事をしています。


## ２－２．イベントループ


**イベントループ**は、コルーチンの実行をスケジュールし、コルーチン同士の実行を切り替えたり、StopIteration例外を処理したりします。それ以上にソケットやファイルのイベントを処理したりもします。

イベントループはasyncioによる非同期システムの心臓ですが、裏方ですのであまり表には出ません。例えばaiohttp_client.pyでは以下のように使われています。

```python
loop = asyncio.get_event_loop() # イベントループの取得
loop.run_until_complete(main()) # コルーチンmain()をイベントループに登録し終了するまで走らせる。
```


## ２－３．タスク

タスクはコルーチンのラッパーで、コルーチンをイベントループに登録しスケジュールしたものです。**asyncio.create_task(コルーチン)** で作成します。

以下の公式サイトの例で説明します。
asyncio.run()でイベントループを初期化・開始させ、 asyncio.create_task()で２つのタスクをイベントループ状に登録し、並行実行しているものです。

```python:asyncio_concurrent.py
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)　# I/Oバウンド、終了するまで制御をイベントループに返します
    print(what)

async def main():
    task1 = asyncio.create_task(  # taskの作成と実行
        say_after(1, 'hello'))

    task2 = asyncio.create_task(　# taskの作成と実行
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1   # task1の実行終了を待つ
    await task2   # task2の実行終了を待つ

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())  # イベントループの作成と実行。コルーチンmain()を走らせる。
```

以下が実行結果です。同期的な処理なら3秒かかるところ、非同期の並行処理なので2秒しかかかっていないのがわかります。asyncio.sleep(delay)が並行して実行されているからです。


```
python asyncio_concurrent.py
started at 09:34:56
hello
world
finished at 09:34:58
```


また **asyncio.gather(\*tasks)** はタスクリストtasksを同時並行処理し、awaitableを返します。



## ２－４．asyncio版コンテキストマネージャ
**async with** でasyncio版**コンテキストマネージャ**を使うことができます。通常のコンテキストマネージャとと違うのは def __enter__ と def __exit__ メソッドの代わりに、async def __aenter__ と async def __aexit__ メソッドを定義することです。またcontextlibモジュールとasynccontextmanagerデコレーターを使ってより簡単に定義できるところも通常のコンテキストマネージャと同様です。

aiohttp_client.pyで言うと以下の2行で asyncio版コンテキストマネージャが使われています。

```python
    async with aiohttp.ClientSession() as session:
    async with session.get(url) as resp:
```


以上で終わります。