11
9

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 1 year has passed since last update.

Python Asyncio入門

Last updated at Posted at 2021-02-02

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

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

Asyncio公式サイト

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

aiohttp公式サイト

以下の関連記事を投稿しました
(追加 2021/02/13) Django 3.1のAsync Views - Qiita
(追加 2022/09/05) Python Asyncio で作る Socket Server

1. ソースコード

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

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入門(1) セットアップ・文字列・数値編 (¥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入門(1) セットアップ・文字列・数値編 (¥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: 1週間の短期間講座!楽しく学ぶRailsの新しい入門書 (¥650)

2. 解説

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

2-1.コルーチン

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

>>> 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() で実行します。

>>> 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()処理など多くの仕事をしています。

2-2.イベントループ

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

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

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

2-3.タスク

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

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

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を返します。

2-4.asyncio版コンテキストマネージャ

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

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

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

以上で終わります。

11
9
0

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
11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?