Pythonで高速化というとアルゴリズムの改善、マルチプロセス、マルチスレッド、Cライブラリ呼出しあたりが効果を見込みやすいですが、外部APIなどの外部リソースの待ち時間がボトルネックの場合は非同期実行を用いるという手も有用です。外部リソースが処理を行っている間、Python側でも処理を行うことである種の並列実行の状態になり単に同期的に処理を行うよりも高速化されます。これの特によいところはソースコードを比較的シンプルに保てるところです。
Python の非同期実行は次の様に async で非同期で実行する関数を定義し、await で同期的に実行する部分を定義、asyncio で非同期の細かい制御、asyncio.run で非同期処理全体を同期処理に引き継がせる、といった段階で定義できます。
import asyncio async def app_sleep(sec): """ 非同期スリープ。与えられた秒数後に完了する関数 :param sec: :return: """ print(f"start{sec}") # asyncio の提供する非同期スリープ。await でこの関数内においては同期的に用いる await asyncio.sleep(sec) print(f"end{sec}") return f"これは app_sleep({sec}) の返り値" async def main(): # 二つの非同期関数を非同期実行して返り値を得る a = app_sleep(5) b = app_sleep(1) [ret_a, ret_b] = await asyncio.gather(a, b) # 返り値を表示 print(ret_a) print(ret_b) if __name__ == '__main__': print('ここは同期処理のはじまり') # asyncio.run は同期処理の中から非同期処理を呼び出せます asyncio.run(main()) print('ここは同期処理のおわり') """ ここは同期処理のはじまり start3 start1 end1 end3 これは app_sleep(3) の返り値 これは app_sleep(1) の返り値 ここは同期処理のおわり """
これを見てよく async 関数全体が非同期実行される様に思ってしまうのですが、実はそうではありません。次の様に同期的な sleep を用いた場合にそれを示せます。
import asyncio import time async def app_sleep(sec): """ 非同期スリープ。与えられた秒数後に完了する関数 :param sec: :return: """ print(f"start{sec}") # time の提供する同期スリープ。プロセス全体がスリープする time.sleep(sec) print(f"end{sec}") return f"これは app_sleep({sec}) の返り値" async def main(): # 二つの非同期関数を非同期実行して返り値を得る a = app_sleep(3) b = app_sleep(1) [ret_a, ret_b] = await asyncio.gather(a, b) # 返り値を表示 print(ret_a) print(ret_b) if __name__ == '__main__': print('ここは同期処理のはじまり') # asyncio.run は同期処理の中から非同期処理を呼び出せます asyncio.run(main()) print('ここは同期処理のおわり') """ ここは同期処理のはじまり start3 end3 start1 end1 これは app_sleep(3) の返り値 これは app_sleep(1) の返り値 ここは同期処理のおわり """
私的にこれは不便なのですが、同期処理を非同期処理にラッピングするrun_in_executor
というメソッドが Python には備わっています。これを使うことでどこでも期待通りの非同期処理を定義できます。
イベントループ — Python 3.10.4 ドキュメント#awaitable loop.run_in_executor(executor, func, *args)¶
import asyncio import time async def app_sleep(sec): """ 非同期スリープ。与えられた秒数後に完了する関数 :param sec: :return: """ print(f"start{sec}") # time の提供する同期スリープを非同期化。await asyncio.sleep の様に動作する await asyncio.get_event_loop().run_in_executor( None, # executor. 並列処理用のリソース。シングルスレッド、シングルプロセスで処理する分には None でOK time.sleep, # 動機関数 sec # 引数 ) print(f"end{sec}") return f"これは app_sleep({sec}) の返り値" async def main(): # 二つの非同期関数を非同期実行して返り値を得る a = app_sleep(3) b = app_sleep(1) [ret_a, ret_b] = await asyncio.gather(a, b) # 返り値を表示 print(ret_a) print(ret_b) if __name__ == '__main__': print('ここは同期処理のはじまり') # asyncio.run は同期処理の中から非同期処理を呼び出せます asyncio.run(main()) print('ここは同期処理のおわり') """ ここは同期処理のはじまり start3 start1 end1 end3 これは app_sleep(3) の返り値 これは app_sleep(1) の返り値 ここは同期処理のおわり """
Python は明示的に記述しない限り PHP を連想するほどの同期処理のプログラムになるので割と使いどころのある書き方です。