はじめに
この実験では、Python の yield from 文を使ってジェネレーターの委任について学びます。この機能は Python 3.3 で導入され、ジェネレーターとコルーチンに依存するコードを簡素化します。
ジェネレーターは、実行を一時停止して再開できる特殊な関数で、呼び出し間で状態を保持します。yield from 文は、制御を別のジェネレーターに委任するエレガントな方法を提供し、コードの可読性と保守性を向上させます。
目的:
yield from文の目的を理解するyield fromを使って他のジェネレーターに委任する方法を学ぶ- この知識を活用して、コルーチンベースのコードを簡素化する
- 現代の async/await 構文との関連性を理解する
操作するファイル:
cofollow.py- コルーチンのユーティリティ関数が含まれていますserver.py- シンプルなネットワークサーバーの実装が含まれています
yield from 文の理解
このステップでは、Python の yield from 文について詳しく調べます。この文は、ジェネレーターを扱う際の強力なツールであり、他のジェネレーターに操作を委任するプロセスを簡素化します。このステップの終わりまでに、yield from が何であるか、どのように動作するか、そして異なるジェネレーター間での値の受け渡しをどのように処理するかを理解するでしょう。
yield from とは何か?
yield from 文は Python 3.3 で導入されました。その主な目的は、サブジェネレーターへの操作の委任を簡素化することです。サブジェネレーターとは、メインのジェネレーターが作業を委任できる別のジェネレーターのことです。
通常、あるジェネレーターが別のジェネレーターから値を生成する場合、ループを使用する必要があります。たとえば、yield from を使わない場合、次のようなコードを書くことになります。
def delegating_generator():
for value in subgenerator():
yield value
このコードでは、delegating_generator が for ループを使って subgenerator が生成する値を反復処理し、それぞれの値を 1 つずつ生成します。
しかし、yield from 文を使うと、コードははるかに簡単になります。
def delegating_generator():
yield from subgenerator()
この 1 行のコードは、前の例のループと同じ結果を達成します。ただし、yield from は単なるショートカットではありません。呼び出し元とサブジェネレーター間の双方向通信も管理します。つまり、委任するジェネレーターに送られた値は、直接サブジェネレーターに渡されます。
基本的な例
yield from が実際にどのように動作するかを見るために、簡単な例を作成しましょう。
- まず、エディタで
cofollow.pyファイルを開く必要があります。これを行うには、cdコマンドを使って正しいディレクトリに移動します。ターミナルで次のコマンドを実行します。
cd /home/labex/project
- 次に、
cofollow.pyファイルに 2 つの関数を追加します。subgen関数は、0 から 4 までの数を生成する単純なジェネレーターです。main_gen関数は、yield fromを使ってこれらの数の生成をsubgenに委任し、その後'Done'という文字列を生成します。cofollow.pyファイルの末尾に次のコードを追加します。
def subgen():
for i in range(5):
yield i
def main_gen():
yield from subgen()
yield 'Done'
- これで、これらの関数をテストしましょう。Python シェルを開き、次のコードを実行します。
from cofollow import subgen, main_gen
## Test subgen directly
for x in subgen():
print(x)
## Test main_gen that delegates to subgen
for x in main_gen():
print(x)
このコードを実行すると、次の出力が表示されるはずです。
0
1
2
3
4
0
1
2
3
4
Done
この出力は、yield from が main_gen から subgen が生成するすべての値を呼び出し元に直接渡すことを可能にすることを示しています。
yield from を使った値の受け渡し
yield from の最も強力な機能の 1 つは、双方向の値の受け渡しを処理する能力です。これを実証するために、もう少し複雑な例を作成しましょう。
cofollow.pyファイルに次の関数を追加します。
def accumulator():
total = 0
while True:
value = yield total
if value is None:
break
total += value
def caller():
acc = accumulator()
yield from acc
yield 'Total accumulated'
accumulator 関数は、累計値を追跡するコルーチンです。現在の累計値を生成し、次の値を受け取るのを待ちます。None を受け取った場合、ループを停止します。caller 関数は accumulator のインスタンスを作成し、yield from を使ってすべての送受信操作を委任します。
- Python シェルでこれらの関数をテストします。
from cofollow import caller
c = caller()
print(next(c)) ## Start the coroutine
print(c.send(1)) ## Send value 1, get accumulated value
print(c.send(2)) ## Send value 2, get accumulated value
print(c.send(3)) ## Send value 3, get accumulated value
print(c.send(None)) ## Send None to exit the accumulator
このコードを実行すると、次の出力が表示されるはずです。
0
1
3
6
'Total accumulated'
この出力は、yield from がサブジェネレーターが使い果たされるまで、すべての送受信操作を完全に委任することを示しています。
これで yield from の基本を理解したので、次のステップでより実用的なアプリケーションに移りましょう。
コルーチンでの yield from の使用
このステップでは、より実用的なアプリケーションのために、コルーチンと共に yield from 文をどのように使用するかを探ります。コルーチンは Python の強力な概念であり、これと yield from を組み合わせる方法を理解することで、コードを大幅に簡素化できます。
コルーチンとメッセージの受け渡し
コルーチンは、yield 文を通じて値を受け取ることができる特殊な関数です。データ処理やイベントハンドリングなどのタスクに非常に役立ちます。cofollow.py ファイルには consumer デコレータがあります。このデコレータは、コルーチンを最初の yield ポイントまで自動的に進めることで、コルーチンのセットアップを支援します。つまり、手動でコルーチンを開始する必要はなく、デコレータがその処理を行ってくれます。
値を受け取り、その型を検証するコルーチンを作成しましょう。以下のように行うことができます。
- まず、エディタで
cofollow.pyファイルを開きます。ターミナルで次のコマンドを使用して、正しいディレクトリに移動できます。
cd /home/labex/project
- 次に、
cofollow.pyファイルの末尾に次のreceive関数を追加します。この関数は、メッセージを受け取り、その型を検証するコルーチンです。
def receive(expected_type):
"""
A coroutine that receives a message and validates its type.
Returns the received message if it matches the expected type.
"""
msg = yield
assert isinstance(msg, expected_type), f'Expected type {expected_type}'
return msg
この関数の動作は以下の通りです。
- 式を伴わない
yieldを使用して値を受け取ります。コルーチンに値が送信されると、このyield文がそれを捕捉します。 isinstance関数を使用して、受け取った値が期待される型であるかどうかを確認します。型が一致しない場合、AssertionErrorを発生させます。- 型チェックが通過すると、値を返します。
- これで、
receive関数と共にyield fromを使用するコルーチンを作成しましょう。この新しいコルーチンは、整数のみを受け取り、表示します。
@consumer
def print_ints():
"""
A coroutine that receives and prints integers only.
Uses yield from to delegate to the receive coroutine.
"""
while True:
val = yield from receive(int)
print('Got:', val)
- このコルーチンをテストするには、Python シェルを開き、次のコードを実行します。
from cofollow import print_ints
p = print_ints()
p.send(42)
p.send(13)
try:
p.send('13') ## This should raise an AssertionError
except AssertionError as e:
print(f"Error: {e}")
以下の出力が表示されるはずです。
Got: 42
Got: 13
Error: Expected type <class 'int'>
コルーチンでの yield from の動作の理解
print_ints コルーチンで yield from receive(int) を使用すると、以下の手順が実行されます。
- 制御が
receiveコルーチンに委任されます。これは、print_intsコルーチンが一時停止し、receiveコルーチンが実行を開始することを意味します。 receiveコルーチンはyieldを使用して値を受け取ります。送信される値を待機します。print_intsに値が送信されると、実際にはreceiveがそれを受け取ります。yield from文がprint_intsからreceiveへの値の受け渡しを処理します。receiveコルーチンは、受け取った値の型を検証します。型が正しい場合、値を返します。- 返された値は、
print_intsコルーチン内のyield from式の結果となります。つまり、print_ints内のval変数には、receiveが返した値が割り当てられます。
yield from を使用すると、直接 yield と受け取りを処理する場合よりもコードが読みやすくなります。コルーチン間の値の受け渡しの複雑さを抽象化します。
より高度な型チェックコルーチンの作成
より複雑な型検証を処理するために、ユーティリティ関数を拡張しましょう。以下のように行うことができます。
cofollow.pyファイルに次の関数を追加します。
def receive_dict():
"""Receive and validate a dictionary"""
result = yield from receive(dict)
return result
def receive_str():
"""Receive and validate a string"""
result = yield from receive(str)
return result
@consumer
def process_data():
"""Process different types of data using the receive utilities"""
while True:
print("Waiting for a string...")
name = yield from receive_str()
print(f"Got string: {name}")
print("Waiting for a dictionary...")
data = yield from receive_dict()
print(f"Got dictionary with {len(data)} items: {data}")
print("Processing complete for this round.")
- 新しいコルーチンをテストするには、Python シェルを開き、次のコードを実行します。
from cofollow import process_data
proc = process_data()
proc.send("John Doe")
proc.send({"age": 30, "city": "New York"})
proc.send("Jane Smith")
try:
proc.send(123) ## This should raise an AssertionError
except AssertionError as e:
print(f"Error: {e}")
以下のような出力が表示されるはずです。
Waiting for a string...
Got string: John Doe
Waiting for a dictionary...
Got dictionary with 2 items: {'age': 30, 'city': 'New York'}
Processing complete for this round.
Waiting for a string...
Got string: Jane Smith
Waiting for a dictionary...
Error: Expected type <class 'dict'>
yield from 文により、コードがよりクリーンで読みやすくなります。コルーチン間のメッセージの受け渡しの詳細に取り組むのではなく、プログラムの高レベルなロジックに集中することができます。
ジェネレーターを使ったソケットのラッピング
このステップでは、ジェネレーターを使ってソケット操作をラッピングする方法を学びます。これは非常に重要な概念であり、特に非同期プログラミングに関係しています。非同期プログラミングにより、プログラムはあるタスクが終了するのを待たずに複数のタスクを同時に処理することができます。ジェネレーターを使ってソケット操作をラッピングすることで、コードをより効率的かつ管理しやすくすることができます。
問題の理解
server.py ファイルには、ジェネレーターを使ったシンプルなネットワークサーバーの実装が含まれています。現在のコードを見てみましょう。このコードはサーバーの基礎であり、何か変更を加える前に理解することが重要です。
def tcp_server(address, handler):
sock = socket(AF_INET, SOCK_STREAM)
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(5)
while True:
yield 'recv', sock
client, addr = sock.accept()
tasks.append(handler(client, addr))
def echo_handler(client, address):
print('Connection from', address)
while True:
yield 'recv', client
data = client.recv(1000)
if not data:
break
yield 'send', client
client.send(b'GOT:' + data)
print('Connection closed')
client.close()
このコードでは、yield キーワードを使用しています。yield キーワードは Python でジェネレーターを作成するために使用されます。ジェネレーターは、関数の実行を一時停止したり再開したりできる特殊なタイプのイテレーターです。ここでは、yield はサーバーが接続を受け入れる準備ができたとき、またはクライアントハンドラーがデータを受信または送信する準備ができたときを示すために使用されています。しかし、手動の yield 文はイベントループの内部動作をユーザーに公開しています。これは、ユーザーがイベントループの動作を知っている必要があり、コードの理解と保守が難しくなることを意味します。
GenSocket クラスの作成
ジェネレーターを使ってソケット操作をラッピングする GenSocket クラスを作成しましょう。これにより、コードがクリーンで読みやすくなります。ソケット操作をクラスにカプセル化することで、イベントループの詳細をユーザーから隠し、サーバーの高レベルなロジックに集中することができます。
- エディタで
server.pyファイルを開きます。
cd /home/labex/project
このコマンドは、カレントディレクトリを server.py ファイルがあるプロジェクトディレクトリに変更します。正しいディレクトリに移動したら、好みのテキストエディタでファイルを開くことができます。
- 既存の関数の前に、ファイルの末尾に次の
GenSocketクラスを追加します。
class GenSocket:
"""
A generator-based wrapper for socket operations.
"""
def __init__(self, sock):
self.sock = sock
def accept(self):
"""Accept a connection and return a new GenSocket"""
yield 'recv', self.sock
client, addr = self.sock.accept()
return GenSocket(client), addr
def recv(self, maxsize):
"""Receive data from the socket"""
yield 'recv', self.sock
return self.sock.recv(maxsize)
def send(self, data):
"""Send data to the socket"""
yield 'send', self.sock
return self.sock.send(data)
def __getattr__(self, name):
"""Forward any other attributes to the underlying socket"""
return getattr(self.sock, name)
この GenSocket クラスは、ソケット操作のラッパーとして機能します。__init__ メソッドは、ソケットオブジェクトでクラスを初期化します。accept、recv、および send メソッドは、対応するソケット操作を実行し、操作が準備できたときを示すために yield を使用します。__getattr__ メソッドは、他の属性を基になるソケットオブジェクトに転送することを可能にします。
- これで、
tcp_serverおよびecho_handler関数をGenSocketクラスを使用するように変更しましょう。
def tcp_server(address, handler):
sock = GenSocket(socket(AF_INET, SOCK_STREAM))
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(5)
while True:
client, addr = yield from sock.accept()
tasks.append(handler(client, addr))
def echo_handler(client, address):
print('Connection from', address)
while True:
data = yield from client.recv(1000)
if not data:
break
yield from client.send(b'GOT:' + data)
print('Connection closed')
client.close()
明示的な yield 'recv', sock および yield 'send', client 文が、よりクリーンな yield from 式に置き換えられたことに注意してください。yield from キーワードは、実行を別のジェネレーターに委任するために使用されます。これにより、コードが読みやすくなり、イベントループの詳細がユーザーから隠されます。今では、コードは通常の関数呼び出しのように見え、ユーザーはイベントループの内部動作を心配する必要がありません。
- サーバーの使用方法を示すために、簡単なテスト関数を追加しましょう。
def run_server():
"""Start the server on port 25000"""
tasks.append(tcp_server(('localhost', 25000), echo_handler))
try:
event_loop()
except KeyboardInterrupt:
print("Server stopped")
if __name__ == '__main__':
print("Starting echo server on port 25000...")
print("Press Ctrl+C to stop")
run_server()
このコードは、より読みやすく保守しやすくなっています。GenSocket クラスは yield ロジックをカプセル化し、サーバーコードがイベントループの詳細ではなく高レベルな流れに集中できるようにします。run_server 関数は、ポート 25000 でサーバーを起動し、KeyboardInterrupt 例外を処理します。これにより、ユーザーは Ctrl+C を押すことでサーバーを停止することができます。
利点の理解
yield from アプローチにはいくつかの利点があります。
- クリーンなコード:ソケット操作は通常の関数呼び出しのように見えます。これにより、特に初心者にとってコードが読みやすく理解しやすくなります。
- 抽象化:イベントループの詳細がユーザーから隠されています。ユーザーはサーバーコードを使用するためにイベントループの動作を知る必要はありません。
- 可読性:コードは何をしているかを表現するのに重点が置かれており、どのようにしているかではなくなっています。これにより、コードが自己説明的になり、保守が容易になります。
- 保守性:イベントループに変更を加えても、サーバーコードを変更する必要はありません。これは、将来イベントループを変更する必要がある場合、サーバーコードに影響を与えることなく行うことができることを意味します。
このパターンは、次のステップで探る現代的な async/await 構文への足がかりとなります。async/await 構文は、Python で非同期コードを書くためのより高度でクリーンな方法であり、yield from パターンを理解することで、より簡単に移行することができます。
ジェネレーターから async/await へ
この最後のステップでは、Python の yield from パターンが現代的な async/await 構文にどのように進化したかを探ります。この進化を理解することは、ジェネレーターと非同期プログラミングの関係を把握する上で重要です。非同期プログラミングにより、プログラムは各タスクが終了するのを待たずに複数のタスクを処理することができ、これはネットワークプログラミングやその他の I/O バウンド操作で特に有用です。
ジェネレーターと async/await の関係
Python 3.5 で導入された async/await 構文は、ジェネレーターと yield from 機能を基に構築されています。内部的には、async 関数はジェネレーターを使って実装されています。これは、あなたが学んだジェネレーターに関する概念が、async/await の動作に直接関係していることを意味します。
ジェネレーターの使用から async/await 構文へ移行するには、以下の手順に従う必要があります。
typesモジュールから@coroutineデコレーターを使用します。このデコレーターは、ジェネレーターベースの関数をasync/awaitで使用できる形式に変換するのに役立ちます。yield fromを使用する関数を、代わりにasyncとawaitを使用するように変換します。これにより、コードが読みやすくなり、操作の非同期的な性質がより明確に表現されます。- イベントループを更新して、ネイティブコルーチンを処理できるようにします。イベントループは、非同期タスクのスケジューリングと実行を担当します。
GenSocket クラスの更新
では、GenSocket クラスを @coroutine デコレーターと共に動作するように変更しましょう。これにより、クラスを async/await コンテキストで使用できるようになります。
- エディタで
server.pyファイルを開きます。ターミナルで次のコマンドを実行することで、これを行うことができます。
cd /home/labex/project
server.pyファイルの先頭に、coroutineのインポートを追加します。このインポートは、@coroutineデコレーターを使用するために必要です。
from types import coroutine
GenSocketクラスを更新して、@coroutineデコレーターを使用するようにします。このデコレーターは、ジェネレーターベースのメソッドをawait可能なコルーチンに変換します。つまり、awaitキーワードと共に使用できるようになります。
class GenSocket:
"""
A generator-based wrapper for socket operations
that works with async/await.
"""
def __init__(self, sock):
self.sock = sock
@coroutine
def accept(self):
"""Accept a connection and return a new GenSocket"""
yield 'recv', self.sock
client, addr = self.sock.accept()
return GenSocket(client), addr
@coroutine
def recv(self, maxsize):
"""Receive data from the socket"""
yield 'recv', self.sock
return self.sock.recv(maxsize)
@coroutine
def send(self, data):
"""Send data to the socket"""
yield 'send', self.sock
return self.sock.send(data)
def __getattr__(self, name):
"""Forward any other attributes to the underlying socket"""
return getattr(self.sock, name)
async/await 構文への変換
次に、サーバーコードを async/await 構文を使用するように変換しましょう。これにより、コードが読みやすくなり、操作の非同期的な性質が明確に表現されます。
async def tcp_server(address, handler):
"""
An asynchronous TCP server using async/await.
"""
sock = GenSocket(socket(AF_INET, SOCK_STREAM))
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(5)
while True:
client, addr = await sock.accept()
tasks.append(handler(client, addr))
async def echo_handler(client, address):
"""
An asynchronous handler for echo clients.
"""
print('Connection from', address)
while True:
data = await client.recv(1000)
if not data:
break
await client.send(b'GOT:' + data)
print('Connection closed')
client.close()
yield from が await に置き換えられ、関数が def ではなく async def で定義されていることに注意してください。この変更により、コードがより直感的で理解しやすくなります。
変換の理解
yield from を使ったジェネレーターから async/await 構文への移行は、単なる構文の変更ではありません。これは、非同期プログラミングに対する考え方の変化を表しています。
yield from を使ったジェネレーター:
yield fromを使ったジェネレーターを使用する場合、タスクが準備できたことを信号するために明示的に制御をyieldします。これは、タスクがいつ続行できるかを手動で管理する必要があることを意味します。- タスクのスケジューリングも手動で管理する必要があります。これは、特に大規模なプログラムでは複雑になる可能性があります。
- 制御フローの仕組みに焦点が当てられており、これによりコードの読み取りと保守が難しくなる可能性があります。
async/await 構文:
async/await構文では、awaitポイントで暗黙的に制御がyieldされます。これにより、明示的に制御をyieldすることを心配する必要がなくなり、コードがよりシンプルになります。- イベントループがタスクのスケジューリングを処理するため、手動で管理する必要はありません。
- プログラムの論理的な流れに焦点が当てられており、これによりコードが読みやすく保守しやすくなります。
この変換により、より読みやすく保守しやすい非同期コードが可能になり、これはネットワークサーバーのような複雑なアプリケーションに特に重要です。
現代的な非同期プログラミング
現代の Python では、通常、カスタムイベントループではなく asyncio モジュールを非同期プログラミングに使用します。asyncio モジュールは、多くの有用な機能を組み込みでサポートしています。
- 複数のコルーチンを同時に実行すること。これにより、プログラムは複数のタスクを同時に処理することができます。
- ネットワーク I/O の管理。ネットワークを介したデータの送受信プロセスを簡素化します。
- 同期プリミティブ。これらは、並行環境で共有リソースへのアクセスを管理するのに役立ちます。
- タスクのスケジューリングとキャンセル。特定の時間にタスクを実行するように簡単にスケジューリングでき、必要に応じてキャンセルすることもできます。
以下は、asyncio を使用した場合のサーバーの例です。
import asyncio
async def handle_client(reader, writer):
addr = writer.get_extra_info('peername')
print(f'Connection from {addr}')
while True:
data = await reader.read(1000)
if not data:
break
writer.write(b'GOT:' + data)
await writer.drain()
print('Connection closed')
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(
handle_client, 'localhost', 25000
)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(main())
このコードは、ジェネレーターベースのサーバーと同じ機能を実現していますが、より堅牢で機能豊富な標準の asyncio ライブラリを使用しています。
まとめ
この実験では、いくつかの重要な概念を学びました。
yield from文と、それが別のジェネレーターに委任する方法。これは、ジェネレーターの動作を理解する上で基本的な概念です。- コルーチンと共に
yield fromを使用してメッセージを受け渡す方法。これにより、非同期プログラムの異なる部分間で通信することができます。 - ジェネレーターを使ってソケット操作をラッピングし、コードをクリーンにする方法。これにより、ネットワーク関連のコードがより整理され、理解しやすくなります。
- ジェネレーターから現代的な
async/await構文への移行。この移行を理解することで、ジェネレーターを直接使用する場合でも、現代的なasync/await構文を使用する場合でも、Python でより読みやすく保守しやすい非同期コードを書くことができます。
まとめ
この実験では、Python のジェネレーターの委任という概念について学びました。特に yield from 文とその様々な応用に焦点を当てました。yield from を使って別のジェネレーターに委任する方法を調べ、これによりコードが簡素化され、可読性が向上することを学びました。また、yield from を使ってコルーチンを作成し、メッセージを受信して検証する方法や、ジェネレーターを使ってソケット操作をラッピングし、ネットワークコードをクリーンにする方法も学びました。
これらの概念は、Python の非同期プログラミングを理解するために不可欠です。ジェネレーターから現代的な async/await 構文への移行は、非同期操作の処理における大きな進歩を表しています。これらの概念をさらに探求するには、asyncio モジュールを学習し、人気のあるフレームワークが async/await をどのように使用しているかを調べ、独自の非同期ライブラリを開発することができます。ジェネレーターの委任と yield from を理解することで、Python の非同期プログラミングのアプローチについてより深い洞察を得ることができます。