aiofilesとasyncioを使用したPythonの非同期ファイル処理

May 13, 2021
執筆者
Sam Agnew
Twilion

aiofiles and asyncio python async file

この記事はTwilioデベロッパーエバンジェリストのSam Agnewが執筆した記事(英語)を日本語化したものです。

非同期コードは、今ではPython開発の主力となっています。asyncioが標準ライブラリーに加わり、互換性のある機能がサードパーティーのパッケージで数多く提供される今、このパラダイムは今後も続くと考えられます。

非同期コードを作成する上で重要な点は、コードのすべてのパーツを上手く連携させることです。ある特定の要素が、他のすべての要素を遅延させる事態とならないよう、上手に連携させましょう。ファイルI/Oは、非同期処理連携においてよくある阻害要因です。そこで、この記事では、aiofilesライブラリーを使用してファイルを非同期処理する方法を説明します。

基本から始めましょう。、ファイルの内容を非同期で読み取るために必要なコードは以下になります。(非同期関数の中にあるものとします。)

async with aiofiles.open('filename', mode='r') as f:
    contents = await f.read()
print(contents)

では、詳しく見ていきます。

ノンブロッキングコードとは?

「非同期」、「ノンブロッキング」、「同時(並行)」はそれぞれよく聞く表現ですが、それぞれの意味は少しまぎらわしいところがあります。こちらのより詳細なチュートリアルによると、主な特性は次の2つです。

  • 非同期ルーチンでは、最終結果を待つ間「一時停止」し、その間に他のルーチンを実行できる。
  • 非同期コードは、上記メカニズムを通じ同時実行を促す。別の言い方をすると、非同期コードは同時実行の操作感を提供する。

このように非同期コードとは、結果待機中に他のコードを実行できるよう、自身の処理を一時停止することができるコードです。他のコードの実行を「ブロック」しないため、「ノンブロッキング」コードと言えます。

asyncioライブラリーには、Python開発者が非同期の処理を行うための、さまざまなツールがあります。またaiofilesには、ファイル操作専用の機能があります。

セットアップ

Python環境をあらかじめセットアップしておきます。必要に応じて、このガイドのvirtualenv(仮想環境)に関するセクションを参照してください。すべてが正常に動作するようにします。特に、仮想環境は、同じマシン上で複数のプロジェクトを実行している場合、dependency設定を分けるうえで重要です。この投稿で示すコードを実行するには、Pythonのバージョンは3.7以上であることが必要です。

環境のセットアップ後に、サードパーティーのライブラリーをいくつかインストールする必要があります。ここではaiohttpを使用します。仮想環境をアクティブにした後、次のコマンドでインストールしてください。

pip install aiofiles==0.6.0

以降の例で利用するPokemon APIは、初代150ポケモンのデータをJSONファイルで保有しています。こちらから、すべてのファイルを格納したフォルダーをダウンロードできます。これで準備が整いました。次のステップに進んでコードを記述しましょう。

aiofilesによるファイル読み取り

まず、特定のポケモンに対応するファイルを開き、JSONファイルを解析して辞書に入れ、名前を出力しましょう。

import aiofiles
import asyncio
import json


async def main():
    async with aiofiles.open('articuno.json', mode='r') as f:
        contents = await f.read()
    pokemon = json.loads(contents)
    print(pokemon['name'])

asyncio.run(main())

このコードを実行すると、ターミナルに「articuno」と出力されます。ファイルを1行ずつ、非同期に反復処理することもできます。(その場合は、articuno.jsonの全9271行が出力されます)

import aiofiles
import asyncio

async def main():
    async with aiofiles.open('articuno.json', mode='r') as f:
        async for line in f:
            print(line)

asyncio.run(main())

aiofilesによるファイル書き込み

ファイルの書き込みも、標準のPythonファイルI/Oと同様です。例えば、各ポケモン達が獲得できるすべての技をリストしたファイルを作成するとしましょう。簡単な例として、「transform(変身)」という技のみを取得できるポケモン Ditt(メタモン)の場合、情報を書き込むコードは以下となります。

import aiofiles
import asyncio

async def main():
    async with aiofiles.open('ditto_moves.txt', mode='w') as f:
        await f.write('transform')

asyncio.run(main())

同様に、次のポケモンRhydon(サイドン)の例を試してみましょう。サイドンは複数の技を持つため、コードは次のようになります。

import aiofiles
import asyncio
import json


async def main():
    # Read the contents of the json file.
    async with aiofiles.open('rhydon.json', mode='r') as f:
        contents = await f.read()

    # Load it into a dictionary and create a list of moves.
    pokemon = json.loads(contents)
    name = pokemon['name']
    moves = [move['move']['name'] for move in pokemon['moves']]

    # Open a new file to write the list of moves into.
    async with aiofiles.open(f'{name}_moves.txt', mode='w') as f:
        await f.write('\n'.join(moves))


asyncio.run(main())

上記のコードを実行した後、rhydon_moves.txtを開いてみましょう。、以下のように112行分のファイル一覧を確認できるはずです。

Rydon"s skill

asyncioを使用した複数ファイルの非同期処理

次はもう少し複雑になります。JSONファイルにある150ポケモンすべてを処理します。すべてのファイルを読み取り、解析してそれぞれのポケモンが持つ技を、新しいファイルに書き込みます。

import aiofiles
import asyncio
import json
from pathlib import Path


directory = 'directory/your/files/are/in'


async def main():
    pathlist = Path(directory).glob('*.json')

    # Iterate through all json files in the directory.
    for path in pathlist:
        # Read the contents of the json file.
        async with aiofiles.open(f'{directory}/{path.name}', mode='r') as f:
            contents = await f.read()

        # Load it into a dictionary and create a list of moves.
        pokemon = json.loads(contents)
        name = pokemon['name']
        moves = [move['move']['name'] for move in pokemon['moves']]

        # Open a new file to write the list of moves into.
        async with aiofiles.open(f'{directory}/{name}_moves.txt', mode='w') as f:
            await f.write('\n'.join(moves))


asyncio.run(main())

このコードを実行すると、ポケモンファイルのディレクトリに.txtファイルと.jsonファイルが出力されます。ファイルには、それぞれのポケモンに対応する技リストが格納されています。

ls command to show json & txt files

例えば、ファイル書き込み後に全ポケモンの技を一覧したいときなど、つまり、いくつかの非同期処理を実行させ、かつそれらの非同期処理を経た後のデータを取得したい場合、asyncio.ensure_future とasyncio.gatherが使用できます。

各ファイルを処理するコード部分を、独自の非同期関数に分割し、それぞれの関数呼び出しに対し、Promiseを追加します。こちらがその関数の例です。このコードには新しいmain関数が含まれています。

async def write_pokemon_moves(filename):
    # Read the contents of the json file.
    async with aiofiles.open(f'{directory}/{filename}', mode='r') as f:
        contents = await f.read()

    # Load it into a dictionary and create a list of moves.
    pokemon = json.loads(contents)
    name = pokemon['name']
    moves = [move['move']['name'] for move in pokemon['moves']]

    # Open a new file to write the list of moves into.
    async with aiofiles.open(f'{directory}/{name}_moves.txt', mode='w') as f:
        await f.write('\n'.join(moves))
    return { 'name': name, 'moves': moves }


async def main():
    pathlist = Path(directory).glob('*.json')

    # A list to be populated with async tasks.
    tasks = []

    # Iterate through all json files in the directory.
    for path in pathlist:
        tasks.append(asyncio.ensure_future(write_pokemon_moves(path.name)))

    # Will contain a list of dictionaries containing Pokemons' names and moves
    moves_list = await asyncio.gather(*tasks)

以上、HTTPリクエストの作成などに使用される、一般的なPythonの非同期コードの使用方法を紹介しました。

どこで利用する?

ポケモンデータを使用した今回の例では、aiofilesモジュールの機能と、ディレクトリ操作とファイルの読み込み/書き込みを行う方法を紹介しました。ファイルI/Oが非同期コード実装のブロッカーとならないコード作成のために、今回紹介したコードをぜひご活用ください。今回、aiohttpとasyncioを使用して何ができるかについてごく初歩的な説明をしました。これを手始めに、非同期Pythonの世界を少しでも身近に感じていただけるようになれば幸いです。

皆さんが何を構築されるか、とても楽しみです。体験談の共有やご質問など、どうぞお気軽にお問い合わせください。