Pythonで以下の様な処理を考える。
f1は引数として関数を受け取り、文字列をprintしたあとにその関数を実行する。
(Python3.7.0で確認)
import time
def f1(termination):
print("[f1] start")
termination()
print("[f1] done")
def main():
f1(lambda: time.sleep(1.0) )
main()
# 実行結果
# [f1] start
# ... 1秒まつ ...
# [f1] done
さて、この処理をasyncioを使って書き直してみる。次の様になる。
import time,asyncio
def f1(termination):
print("[f1] start")
termination()
print("[f1] done")
async def main():
f1(lambda: time.sleep(1.0) )
asyncio.run(main())
ここでの変更点は
-
main
,f1
の定義にasync
をつけ、coroutineを返す関数(coroutine function)にする。 -
main()
を呼ぶ部分をasyncio.run(main())
で置き換える
の2つである。最初のコードと全く同じ様に動作する。
さて、ここでf1に引数で渡している関数の中でawait
したいとしよう。
実は次のように書くとうまくいかない。
import time,asyncio
def f1(termination):
print("[f1] start")
termination()
print("[f1] done")
async def main():
f1(lambda: await asyncio.sleep(1.0) )
asyncio.run(main())
File "nest2.py", line 9
f1(lambda: await asyncio.sleep(1.0) )
^
SyntaxError: 'await' outside async function
これは、awaitを行なっているのが同期関数(普通のdefで定義された関数)の中だから。これを適切に動かすためには、以下の様にf1もcoroutineにする必要がある。
import time,asyncio
async def f1(callback): # asyncをつけた
print("[f1] start")
await callback() # awaitをつけた
print("[f1] done")
async def main(): # asyncをつけた
async def callback():
await asyncio.sleep(1.0)
await f1( callback ) # awaitをつけた
asyncio.run(main())
f1が通常の(同期)関数の場合にはどんなに頑張ってもその配下でawait
を呼ぶことはPythonの文法的に不可能となっている。
main
はcoroutineで、f1はその中でしか呼ばれないので必ずcoroutine内で呼ばれているのであるが、def
を使った処理の中ではawait
を呼ぶことはできない。
この文法が問題になるケース
実はこの文法が問題になるケースがある。
上記のような簡単な場合は、おとなしくf1にもasync
をつければいいだけだが、f1が何重にもネストしているようなライブラリの関数だったとする。
その場合、ネストしている関数すべてにasync
をつけて、その呼び出し部分すべてにawait
をつけなくてはいけない。(例えば"main"->"f1"->"f2"-> .... -> callback
と呼ばれる様な場合は、ネストしているすべての関数の変更が必要。)
(雑感)awaitがあるときにネストした先からイベントループに処理を戻せばよいので、処理系の実装上はできても良さそうなものだが、Pythonの文法をデザインする段階でこういう方針が取られたようだ。残念。javascriptの場合はPromise()が帰ってくるだけなのでネストした先でもawaitできる(と思う)