Pythonで非同期メモ
全然わかってないので基礎的なところから。
- イテレータ
- ジェネレータ
- コルーチン
- async/await
全部しっかりカバーするつもりはなくてasync/awaitを使うためのほんとうに最低限の知識を得るのが目的。なので、単なる写経&メモ。
イテレータ
iterでリストからイテレータオブジェクトを作成する
list_obj = [1, 2, 3, 4]
iter_obj = iter(list_obj)
print(iter_obj)
print(list(iter_obj))
<list_iterator object at 0x7aa81c22bfd0>
[1, 2, 3, 4]
nextでイテレータから要素を取り出せる。全部取り出すと、exception StopIterationが返る。
list_obj = [1, 2, 3, 4]
iter_obj = iter(list_obj)
print(next(iter_obj))
print(next(iter_obj))
print(next(iter_obj))
print(next(iter_obj))
print(next(iter_obj))
1
2
3
4
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-63-f707ae78c6fb> in <cell line: 7>()
5 print(next(iter_obj))
6 print(next(iter_obj))
----> 7 print(next(iter_obj))
StopIteration:
iterやnextを使わずに、forで取り出すのが一般的
list_obj = [1, 2, 3, 4]
iter_obj = iter(list_obj)
for i in iter_obj:
print(i)
1
2
3
4
あとはクラスで使う。以下はイテレータではなくて属性から直接取り出している。
class Fruits:
def __init__(self, fruits_list):
self._fruits_list = fruits_list
fruits = Fruits(["apple", "orange", "banana", "melon", "pineapple"])
for fruit in fruits._fruits_list:
print(fruit)
apple
orange
banana
melon
pineapple
イテレータとして取り出す。
class Fruits:
def __init__(self, fruits_list):
self._fruits_list = fruits_list
def __iter__(self):
return iter(self._fruits_list)
fruits = Fruits(["apple", "orange", "banana", "melon", "pineapple"])
for fruit in fruits:
print(fruit)
apple
orange
banana
melon
pineapple
nextを実装する。
class Fruits:
def __init__(self, fruits_list):
self._fruits_list = fruits_list
def __iter__(self):
return self
def __next__(self):
if len(self._fruits_list):
return self._fruits_list.pop(0)
else:
raise StopIteration()
fruits = Fruits(["apple", "orange", "banana", "melon", "pineapple"])
print(next(fruits))
print(next(fruits))
print(next(fruits))
print(next(fruits))
print(next(fruits))
print(next(fruits))
apple
orange
banana
melon
pineapple
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-92-b7e4e0d3c6e3> in <cell line: 21>()
19 print(next(fruits))
20 print(next(fruits))
---> 21 print(next(fruits))
<ipython-input-92-b7e4e0d3c6e3> in __next__(self)
10 return self._fruits_list.pop(0)
11 else:
---> 12 raise StopIteration
13
14 fruits = Fruits(["apple", "orange", "banana", "melon", "pineapple"])
StopIteration:
fruits = Fruits(["apple", "orange", "banana", "melon", "pineapple"])
for fruit in fruits:
print(fruit)
apple
orange
banana
melon
pineapple
もちょっといろいろあるけれど、イテレータは一旦ここまで。
ジェネレータ
関数だけで実現できる。
def gen():
yield 1
yield 2
yield 3
yield 4
g = gen()
print(type(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
<class 'generator'>
1
2
3
4
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-100-713e0b6b9cbd> in <cell line: 13>()
11 print(next(g))
12 print(next(g))
---> 13 print(next(g))
StopIteration:
def gen():
yield 1
yield 2
yield 3
yield 4
g = gen()
for i in g:
print(i)
1
2
3
4
イテレータと同様に使えるのがわかる。
無限に繰り返すこともできる。
def gen():
i = 0
while True:
i += 1
yield i
g = gen()
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
1
2
3
4
5
for i in g:
print(i)
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(snip)
イテレータとジェネレータの違いは、今の自分の理解だとこう。
- イテレータをクラスで実装した際には「状態」をクラスの属性で持たせて「値を順次返して」ていた。
- ジェネレータは同じことを関数っぽい形で実現している。yieldが実行されるたびに値が返され、処理がそこで中断する。つまり、「状態」を持っていて「値が順次返されている」と言える。
- ただし、関数をそのまま実行するのではなく、関数からジェネレータオブジェクトを作るので、関数よりもむしろクラスからインスタンスを作成するのに近い。
コルーチン
コルーチンの定義はwikipediaより。
コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。
サブルーチンと異なり、状態管理を意識せずに行えるため、協調的処理、イテレータ、無限リスト、パイプなど、継続状況を持つプログラムが容易に記述できる。
つまり、ジェネレータもコルーチンの一種。
ジェネレータはデータを返すだけでなくsendで受け取ることもできる。
def coroutine():
print("start")
while True:
x = yield
print("x:", x)
c = coroutine()
print(type(c))
print(next(c))
print(c.send(1))
print(c.send(2))
print(next(c))
<class 'generator'> #
start
None
x: 1
None
x: 2
None
x: None
None
yieldで受けつつ返すとかできるのね。カウントを途中で変更できるカウンターを作ってみた。
def coroutine_counter():
count = 0
print(f"[DEBUG] start: {count}")
while True:
received = yield count # 現在のカウントを返し、新しい値を待つ
if received is not None:
print(f"[DEBUG] count reset -> {received}")
count = received # sendで送られた値でカウントを更新
else:
print(f"[DEBUG] count up: {count}->{count+1}")
count += 1 # nextで呼ばれた場合はカウントアップ
c = coroutine_counter()
print(next(c)) # コルーチン開始。初回は必ず実施する必要がある
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(c.send(0))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(c.send(10))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
[DEBUG] start: 0
0
[DEBUG] count up: 0->1
1
[DEBUG] count up: 1->2
2
[DEBUG] count up: 2->3
3
[DEBUG] count up: 3->4
4
[DEBUG] count reset -> 0
0
[DEBUG] count up: 0->1
1
[DEBUG] count up: 1->2
2
[DEBUG] count up: 2->3
3
[DEBUG] count up: 3->4
4
[DEBUG] count reset -> 10
10
[DEBUG] count up: 10->11
11
[DEBUG] count up: 11->12
12
[DEBUG] count up: 12->13
13
[DEBUG] count up: 13->14
14
ジェネレータをコルーチンとして使うと、ひとかたまりの処理に、状態をもたせつつ、処理の中断・再開をコントロールできるということになる。
なんとなく並行処理とか非同期処理ができそうな雰囲気が出てきた。
あんまりいい例ではないと思うけども、1つの関数から複数のコルーチンを管理・実行する並行処理のサンプルを書いてみた。
import random
def coroutine_hp_counter(hp_counter_name, initial_hp):
count = initial_hp
while True:
print(f"{hp_counter_name} のげんざいのHP: {count}")
received = yield count
if received is not None:
if received < 0:
received = abs(received)
count -= received
print(f"{hp_counter_name} は {received} のダメージ!")
if count <= 0:
print(f"{hp_counter_name} はしんでしまった")
return 0 #
elif received > 0:
count += received
print(f"{hp_counter_name} は HPが {received} かいふくした: ")
tasks = [coroutine_hp_counter("ゆうしゃ", 100), coroutine_hp_counter("スライム", 20) ]
def run():
while tasks:
t = tasks.pop(0)
try:
t.send(None)
t.send(random.randint(-10, -5))
tasks.append(t)
except StopIteration:
raise
run()
ゆうしゃ のげんざいのHP: 100
ゆうしゃ は 8 のダメージ!
ゆうしゃ のげんざいのHP: 92
スライム のげんざいのHP: 20
スライム は 9 のダメージ!
スライム のげんざいのHP: 11
ゆうしゃ のげんざいのHP: 92
ゆうしゃ は 7 のダメージ!
ゆうしゃ のげんざいのHP: 85
スライム のげんざいのHP: 11
スライム は 8 のダメージ!
スライム のげんざいのHP: 3
ゆうしゃ のげんざいのHP: 85
ゆうしゃ は 7 のダメージ!
ゆうしゃ のげんざいのHP: 78
スライム のげんざいのHP: 3
スライム は 10 のダメージ!
スライム はしんでしまった
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-94-633b8060712b> in <cell line: 32>()
30 raise
31
---> 32 run()
<ipython-input-94-633b8060712b> in run()
25 try:
26 t.send(None)
---> 27 t.send(random.randint(-10, -5))
28 tasks.append(t)
29 except StopIteration:
StopIteration: 0
yield from
を使うとジェネレータのネストというか委譲ができる・・・らしいのだけど、まだこのメリット感がちょっとよくわかっていない。基礎知識が足りない。
が、一旦置いておく。コルーチンがどういうものか、の概念をふんわり理解したかっただけ。
async/await
やっと本題。要はasync defで指定する関数は非同期関数≒コルーチンだというところが、ここまで順にやってきた流れだったりする。(実際にはasync/await->コルーチン?->ジェネレータ?->イテレータ?、みたいなyak shavingなんだけど)
import asyncio
import time
# colab/jupyterなら以下2行を追加
import nest_asyncio
nest_asyncio.apply()
async def say_after(delay: int, what: str) -> None:
print(f"{time.strftime('%X')}: called what: {what}, delay: {delay}")
await asyncio.sleep(delay) # time.sleepだとブロッキングされてしまうのでasyncio.sleepを使う
print(f"{time.strftime('%X')}: say \"{what}\"")
async def main() -> None:
print(f"{time.strftime('%X')} - start")
await asyncio.gather(say_after(10, "hello"), say_after(20, "world"))
print(f"{time.strftime('%X')} - finish")
asyncio.run(main())
03:17:02 - start
03:17:02: called what: hello, delay: 10
03:17:02: called what: world, delay: 20
03:17:12: say "hello"
03:17:22: say "world"
03:17:22 - finish
generatorコルーチンがsend()が呼び出されるまで実行されないように、async/awaitもawaitの処理が終わるまで待っているという点では同じようです。 しかし、行ったり来たり出来ない点ではnative coroutineは、Python以外での文脈で使われる「コルーチン」とは違うようです。
このコードの処理のフロー・依存関係は下記の通りになります。asyncio.gatherで複数のコルーチンをまとめた後、asyncio.run()で実行し、すべての処理が終わるまで待ちます。
ここでは、asyncio.gatherによって複数のコルーチンを同時に、実行し指定されたすべてのコルーチンが完了するのを待ちます。
- 上記の処理を実行中、asyncioモジュール内部にあるイベントループは、複数のコルーチンを管理するためのキューを持ちます。
- このキューには、実行待ちのタスクが順番に格納されています。
- イベントループは、キューからタスクを一つずつ取り出して実行します。
- コルーチンがawaitを使用して非同期操作を行うと、そのコルーチンは一時的に中断されます。
- 例えば、1秒や2秒の待機が指示された場合、その期間中にコルーチンは中断されます。
- この中断中に、イベントループは他の待機中のコルーチンやタスクを実行します。
- 待機が終了した後、中断されていたコルーチンは再開され、その後の処理が続行されます。
図があってめちゃめちゃわかりやすい