Closed6

Pythonで非同期メモ

kun432kun432

全然わかってないので基礎的なところから。

  • イテレータ
  • ジェネレータ
  • コルーチン
  • async/await

全部しっかりカバーするつもりはなくてasync/awaitを使うためのほんとうに最低限の知識を得るのが目的。なので、単なる写経&メモ。

kun432kun432

イテレータ

https://python.ms/iterator/#_1-イテレータを触ってみる。

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

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

もちょっといろいろあるけれど、イテレータは一旦ここまで。

kun432kun432

ジェネレータ

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

https://qiita.com/keitakurita/items/5a31b902db6adfa45a70

関数だけで実現できる。

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が実行されるたびに値が返され、処理がそこで中断する。つまり、「状態」を持っていて「値が順次返されている」と言える。
    • ただし、関数をそのまま実行するのではなく、関数からジェネレータオブジェクトを作るので、関数よりもむしろクラスからインスタンスを作成するのに近い。
kun432kun432

コルーチン

https://zenn.dev/fitness_densuke/articles/understanding_coroutine_in_python

https://qiita.com/koshigoe/items/054383a89bd51d099f10

https://mizzsugar.hatenablog.com/entry/2021/12/05/122537

コルーチンの定義は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を使うとジェネレータのネストというか委譲ができる・・・らしいのだけど、まだこのメリット感がちょっとよくわかっていない。基礎知識が足りない。

が、一旦置いておく。コルーチンがどういうものか、の概念をふんわり理解したかっただけ。

kun432kun432

async/await

やっと本題。要はasync defで指定する関数は非同期関数≒コルーチンだというところが、ここまで順にやってきた流れだったりする。(実際にはasync/await->コルーチン?->ジェネレータ?->イテレータ?、みたいなyak shavingなんだけど)

https://zenn.dev/kevinrobot34/articles/python-asyncio-memo

https://zenn.dev/commmune/articles/19296b87231ea8

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以外での文脈で使われる「コルーチン」とは違うようです。

https://mizzsugar.hatenablog.com/entry/2021/12/05/122537

このコードの処理のフロー・依存関係は下記の通りになります。asyncio.gatherで複数のコルーチンをまとめた後、asyncio.run()で実行し、すべての処理が終わるまで待ちます。

https://zenn.dev/commmune/articles/19296b87231ea8

ここでは、asyncio.gatherによって複数のコルーチンを同時に、実行し指定されたすべてのコルーチンが完了するのを待ちます。

  • 上記の処理を実行中、asyncioモジュール内部にあるイベントループは、複数のコルーチンを管理するためのキューを持ちます。
  • このキューには、実行待ちのタスクが順番に格納されています。
  • イベントループは、キューからタスクを一つずつ取り出して実行します。
  • コルーチンがawaitを使用して非同期操作を行うと、そのコルーチンは一時的に中断されます。
    • 例えば、1秒や2秒の待機が指示された場合、その期間中にコルーチンは中断されます。
  • この中断中に、イベントループは他の待機中のコルーチンやタスクを実行します。
  • 待機が終了した後、中断されていたコルーチンは再開され、その後の処理が続行されます。

https://zenn.dev/fitness_densuke/articles/understanding_coroutine_in_python

このスクラップは2ヶ月前にクローズされました