Pythonのasync/await

Pythonの`async`と`await`キーワードは非同期タスクを管理するために使用されます。

非同期プログラミングは、他のタスクをブロックすることなく複数のタスクを同時に処理できるようにします。Pythonでは、コルーチンを使用してこれを実現します。

`async`キーワードは、関数を非同期コルーチンとして定義するために使用され、`await`キーワードはコルーチン関数内で別のコルーチンの実行を待つために使用されます。

### コルーチンとは何か？
コルーチンは、特定のポイントで実行を一時停止し、再開する機能を持つ通常の関数とほぼ同じです。これにより、非ブロッキング動作が可能になり、コルーチンの実行が遅延した場合や、他のコードを実行するために意図的に実行を一時停止することができます。

コルーチンはジェネレータ関数と非常に密接に関連しています。実際、Python 3.5で`async`と`await`が導入される以前は、ジェネレータ関数を特別な方法で使用してコルーチンを実装していました。

Python 3.5以降のバージョンでは、`async`と`await`という2つのキーワードを使用して、コルーチンをより便利かつネイティブに作成できるようになりました。

- `async`はコルーチン関数を作成します。
- `await`は他のコルーチンを実行するためにコルーチンを一時停止させます。

### `async`を使ってコルーチン関数を定義する
コルーチン関数の定義は、通常の関数の定義に非常に似ています。次の例を考えてみましょう。

#### 例: 通常の関数を定義する
```python
def add(a, b):
    print(f'{a} + {b} = {a + b}')
```

通常の関数では、宣言時に`def`キーワードを使用しますが、コルーチン関数の場合は`def`の代わりに`async def`を使用します。

#### 例: コルーチン関数を定義する
```python
async def add(a, b):
    print(f'{a} + {b} = {a + b}')
```

### コルーチン関数を呼び出す
通常の関数を呼び出すには、その名前の後に必要な引数を括弧で囲んで使用します。

#### 例: 通常の関数を呼び出す
```python
def add(a, b):
    print(f'{a} + {b} = {a + b}')

add(10, 20)
```
**出力**:
```
10 + 20 = 30
```

通常の関数とは異なり、コルーチン関数を呼び出しても、すぐに関数内のコードが実行されるわけではありません。代わりに、コルーチンオブジェクトが作成されます。

#### 例:
```python
async def add(a, b):
    print(f'{a} + {b} = {a + b}')

# コルーチンを呼び出す
coro = add(10, 20) # コルーチンオブジェクトを作成する
print(coro)
```
**出力**:
```
<coroutine object add at 0x7f55ab5b3c40>
```

コルーチン関数内のコードを実行するには、イベントループと呼ばれるもので待機する必要があります。最も直接的な方法は、標準ライブラリの`asyncio`モジュールの`run()`関数を使用することです。

#### 例:
```python
import asyncio

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

# コルーチンオブジェクトを作成する
coro = add(10, 20)

# コルーチンを実行する
asyncio.run(coro) # コルーチン内のコードを実行する
```
**出力**:
```
10 + 20 = 30
```

`asyncio.run()`関数はイベントループを作成し、指定されたコルーチンを実行してその結果を返します。これは非同期アプリケーションのメイン関数を実行するため、または単一のコルーチンを実行するために設計されています。

`asyncio.run()`を使用しない場合、イベントループを手動で初期化および管理し、タスクを作成およびスケジュールし、コードをコルーチンでラップする必要があり、これに比べると`asyncio.run()`を使用する方が複雑さやエラーの発生リスクが低くなります。

#### 例: 他の例
```python
import asyncio

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

asyncio.run(add(50, 30))
asyncio.run(add(50, 50))
asyncio.run(add(90, 10))
```
**出力**:
```
50 + 30 = 80
50 + 50 = 100
90 + 10 = 100
```

### awaitでコルーチンを一時停止させる
`await`キーワードは、別の内部コルーチンが終了するまでコルーチンの実行を一時停止するために使用されます。待機しているコルーチンが終了すると、元のコルーチンは中断した場所から実行を再開します。

**構文**:
```
await <coro()>
```

`await`キーワードは、待機するコルーチンオブジェクト`coro()`を1つだけ引数に取ります。

次の例を考えてみましょう。

#### 例: コルーチンを待機する
```python
import asyncio

# 0からnまでの偶数を表示する
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)

async def main():
      print("Started!")
      await display_evens(10) # await display_evens()
      print('Finished!')

# メインを実行する
asyncio.run(main())
```
**出力**:
```
Started!
0
2
4
6
8
Finished!
```

上記の例では、`await`文に遭遇したとき、`main`の実行は`display_evens()`コルーチンが完全に実行されるまで一時停止します。必要に応じて複数のコルーチンを待機することができます。

#### 例: 複数のコルーチンを待機する
```python
import asyncio

# 0からnまでの偶数を表示する
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)
        await asyncio.sleep(0.01) # 少しの遅延を発生させる

# 0からnまでの奇数を表示する
async def display_odds(n):
    for i in range(n):
        if i % 2 == 1:
            print(i)
        await asyncio.sleep(0.01) # 少しの遅延を発生させる

async def main():
      print("Started!")
      await display_evens(10)
      await display_odds(10) 
      print('Finished!')

# メインを実行する
asyncio.run(main())
```
**出力**:
```
Started!
0
2
4
6
8
1
3
5
7
9
Finished!
```

上記の例では、`await`されたコルーチン`display_evens()`と`display_odds()`は、順番に実行されます。次のセクションでは、`await`されたコルーチンを同時に実行する方法を見てみましょう。

### `await`されたコルーチンを並行して実行する
非同期プログラミングの主な目標は、複数のタスクを並行して実行することです。これにより、あるタスクが実行されている間に、他のタスクも同時に開始し実行できます。

複数のコルーチンを並行して非ブロッキング方式で実行するには、`asyncio.gather()`関数を使用して1つの`awaitable`オブジェクトに結合し、次にそのオ

ブジェクトを`await`します。

#### 例: 複数のコルーチンを実行する
```python
import asyncio

# 0からnまでの偶数を表示する
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)
        await asyncio.sleep(0.01) # 少しの遅延を発生させる

# 0からnまでの奇数を表示する
async def display_odds(n):
    for i in range(n):
        if i % 2 == 1:
            print(i)
        await asyncio.sleep(0.01) # 少しの遅延を発生させる

async def main():
      print("Started!")
      await asyncio.gather(display_evens(10), display_odds(10)) # gatherされたコルーチンは並行して実行される
      print('Finished!')

# メインを実行する
asyncio.run(main())
```
**出力**:
```
Started!
0
1
2
3
4
5
6
7
8
9
Finished!
```

上記の出力を見ると、`display_evens()`と`display_odds()`が同時に実行されていることがわかります。両方の関数からの出力が交互に表示され、すべての整数を印刷する単一の関数が実行されているかのように見えます。


関連記事
[Pythonにおける非同期プログラミング](https://www.pynerds.com/asynchronous-programming-with-python/)

[`asyncio`モジュール ](https://www.pynerds.com/how-to-use-asyncio-in-python/)