見出し画像

Python 3: Deep Dive (Part 2 - Iterators, Generators): コルーチン (セクション13-1/14)

  • Pythonのジェネレータベースのコルーチンは、Python 3.8で非推奨となったが、非同期プログラミングの歴史的進化を理解する上で重要な概念である。

  • コルーチンとは実行を一時停止して再開できる関数で、単一スレッド内での並行処理を実現し、特に協調型マルチタスクを通じて効率的な非同期処理を可能にする。

  • 現代のPythonでは`async`/`await`構文が推奨されるが、レガシーコードのメンテナンスやPythonの非同期処理の深い理解のために、ジェネレータベースのコルーチンの仕組みを知ることは依然として価値がある。

Pythonは長年にわたって大きく進化してきた多目的なプログラミング言語である。初期のバージョンで導入された強力な機能の1つが、非同期プログラミングのためのジェネレータベースのコルーチンの使用であった。しかし、Python 3.5で`async`と`await`構文が導入され、その後Python 3.8でジェネレータベースのコルーチンが非推奨となったため、Pythonにおける現代の非同期プログラミングにつながった歴史的な背景と基礎概念の両方を理解することが重要である。

このブログ記事では、「Python 3: Deep Dive (Part 2 - Iterators, Generators)」「セクション13: コルーチンとしてのジェネレータ」からの教訓を深く掘り下げていく。コルーチンとは何か、どのように機能するのか、そして特にレガシーなPythonコードの保守やアップグレード時に、なぜその理解が今でも価値があるのかを探っていく。


コルーチンの紹介

コルーチンに深く入る前に、ジェネレータベースのコルーチンはPython 3.8で非推奨となり、Python 3.10で削除される予定であることに注意することが重要である。Pythonにおける現代の非同期プログラミングのアプローチは、`asyncio`ライブラリと`async`および`await`キーワードを通じて行われる。

しかし、ジェネレータベースのコルーチンを理解することは、Pythonにおける非同期プログラミングの進化について貴重な洞察を提供し、このパターンを依然として使用しているレガシーコードベースの保守に役立つ。

コルーチンとは何か?

コルーチンはサブルーチン(または関数)の一般化である。単一の入口点と出口点を持つサブルーチンとは異なり、コルーチンは特定のポイントで実行を一時停止し再開することができる。これにより、他のコルーチンと協調することが可能となり、単一のスレッド内での並行性を実現できる。

より簡単に言えば、コルーチンはreturn に到達する前に実行を一時停止できる関数であり、間接的に一定時間の制御を別のコルーチンに渡すことができる。


並行性と並列性

コルーチンを理解するには、プログラミングにおける2つの基本的な概念である並行性並列性を把握する必要がある。

並行性

  • 定義: 並行性とは、多くのことを同時に処理することに関するものである。これは、実行を交互に行うことで複数のタスクを管理し、同時に実行されているという錯覚を与える。

  • 実践: 並行性では、タスクは重複する時間帯で開始、実行、完了するが、必ずしも同時である必要はない。

  • : 単一コアCPUが複数のアプリケーション間を急速に切り替えることで実行する場合。

並列性

  • 定義: 並列性とは、多くのことを同時に行うことである。これには複数の処理ユニット(マルチコアCPUなど)を持つハードウェアが必要である。

  • 実践: 並列性では、複数のタスクが異なるプロセッサーやコア上で正確に同時に実行される。

  • : マルチコアCPUが各コアで異なるタスクを同時に実行する場合。

Pythonに関する注意: グローバルインタプリタロック(GIL)により、PythonスレッドはCPUバウンドタスクの真の並列性を実現できない。ただし、I/OバウンドタスクではGILの影響を受けにくく、並行性によってパフォーマンスを改善できる。


協調型マルチタスクと先制型マルチタスク

協調型マルチタスク

  • 定義: タスクが定期的にまたはアイドル時に自発的に制御を譲り渡すことで、複数のアプリケーションの並行実行を可能にする。

  • 特徴:

    • 各タスクが他のタスクへの制御の譲渡に責任を持つ。

    • 実装が容易だが、適切に振る舞うタスクが必要。

    • 開発者がタスクの譲渡タイミングを制御するコルーチンで使用される。

先制型マルチタスク

  • 定義: オペレーティングシステムがコンテキストスイッチのタイミングを決定し、現在のタスクの協力なしにそれを中断する。

  • 特徴:

    • タスクはいつでも中断される可能性がある。

    • 同期メカニズムの必要性により複雑になる。

    • マルチスレッドやマルチプロセスで一般的。

PythonのコルーチンはCoroutinesは協調型マルチタスクに基づいており、スレッドのオーバーヘッドなしで効率的な並行性を可能にする。


ジェネレータをコルーチンとして使用する

Pythonのジェネレータは主にイテレータを作成するために使用される。しかし、`yield`文を使用して値を生成および消費の両方を行うことで、コルーチンとしても使用できる。

基本的なジェネレータの動作

シンプルなジェネレータ関数の例:

def simple_generator():
    yield 'First'
    yield 'Second'
    yield 'Third'

ジェネレータの使用:

gen = simple_generator()
print(next(gen))  # 出力: First
print(next(gen))  # 出力: Second
print(next(gen))  # 出力: Third

ジェネレータをコルーチンとして使用

`yield`を式として使用することで、ジェネレータは値を生成するだけでなく、受け取ることもできる。

例: 移動平均を計算するコルーチン

def running_averager():
    total = 0
    count = 0
    average = None
    while True:
        new_value = yield average
        total += new_value
        count += 1
        average = total / count

コルーチンの使用:

avg_gen = running_averager()
next(avg_gen)  # ジェネレータの準備
print(avg_gen.send(10))  # 出力: 10.0
print(avg_gen.send(20))  # 出力: 15.0
print(avg_gen.send(30))  # 出力: 20.0

説明:

  • ジェネレータの準備: 値を送信する前に、`next(avg_gen)`を呼び出して最初の`yield`まで進める。

  • データの送信: `send(value)`を使用してジェネレータにデータを渡し、次の`yield`に到達するまで実行を再開する。


プロデューサー・コンシューマーパターンとジェネレータ

キューとスタックの理解

  • キュー:

    • 先入れ先出し(FIFO)のデータ構造。

    • 要素は後ろに追加され、前から削除される。

  • スタック:

    • 後入れ先出し(LIFO)のデータ構造。

    • 要素は上部から追加および削除される。

効率的なキューのための`deque`の使用

Pythonの`collections`モジュールは、両端からの効率的な挿入と削除のための`deque`(双方向キュー)クラスを提供する。

:

from collections import deque

queue = deque(maxlen=10)  # 最大サイズ10のキュー

# 要素の追加
queue.append(1)       # 右側に追加
queue.appendleft(0)   # 左側に追加

# 要素の削除
queue.pop()           # 右側から削除
queue.popleft()       # 左側から削除

コルーチンを使用したプロデューサー・コンシューマーの実装

プロデューサーコルーチン:

def producer(queue, n):
    for i in range(1, n + 1):
        queue.appendleft(i)
        if len(queue) == queue.maxlen:
            print('キューが満杯 - 制御を譲渡')
            yield  # キューが満杯の時に制御を譲渡

コンシューマーコルーチン:

def consumer(queue):
    while True:
        while queue:
            item = queue.pop()
            print(f'アイテムを処理中: {item}')
        print('キューが空 - 制御を譲渡')
        yield  # キューが空の時に制御を譲渡

コーディネーター関数:

def coordinator():
    queue = deque(maxlen=10)
    prod = producer(queue, 35)
    cons = consumer(queue)
    while True:
        try:
            print('生成中...')
            next(prod)
        except StopIteration:
            print('プロデューサー完了')
            break
        finally:
            print('消費中...')
            next(cons)

コーディネーターの実行:

coordinator()

説明:

  • プロデューサー: キューにアイテムを追加し、キューが満杯になると制御を譲渡する。

  • コンシューマー: キューからアイテムを処理し、キューが空になると制御を譲渡する。

  • コーディネーター: プロデューサーとコンシューマーのコルーチンを管理し、効果的な協調を確保する。


ジェネレータの状態とライフサイクル

ジェネレータはそのライフサイクルにおいて異なる状態を持つ:

  1. 作成済み: ジェネレータが作成されたが、まだ開始されていない。

  2. 実行中: ジェネレータが現在実行中である。

  3. 一時停止: ジェネレータが yield され、再開を待機している。

  4. 終了: ジェネレータが実行を完了している。

ジェネレータの状態の検査

Pythonの`inspect`モジュールはジェネレータの状態を確認する方法を提供する。

:

import inspect

def simple_gen():
    yield 'Hello'

gen = simple_gen()
print(inspect.getgeneratorstate(gen))  # 出力: GEN_CREATED

next(gen)
print(inspect.getgeneratorstate(gen))  # 出力: GEN_SUSPENDED

try:
    next(gen)
except StopIteration:
    pass
print(inspect.getgeneratorstate(gen))  # 出力: GEN_CLOSED

結論

ジェネレータベースのコルーチンは現代のPythonバージョンでは非推奨となっているが、それらを理解することは非同期プログラミングの基礎に関する貴重な洞察を提供する。この知識は特に以下の場合に有用である:

  • レガシーコードの保守: 古いPythonコードベースは依然としてジェネレータベースのコルーチンを使用している可能性がある。

  • コア概念の把握: コルーチンの仕組みを理解することで、Pythonの非同期機能についての理解が深まる。

  • 現代のAsyncioへの移行: コルーチンの制限と進化を認識することは、`async`と`await`の効果的な採用に役立つ。

今後に向けて:

  • `asyncio`を学ぶ: Pythonで非同期コードを書く現代的な方法である`async`と`await`構文に慣れる。

  • 非同期ライブラリを探求する: `aiohttp`、`asyncpg`などのライブラリは、高性能な非同期操作のためにasyncioを活用している。

  • 最新情報を把握する: 新機能とベストプラクティスを活用するため、Pythonの開発動向を注視する。


ハッピーコーディング!


参考文献


「超本当にドラゴン」へ

いいなと思ったら応援しよう!