PEP 38 で頭を包むのに苦労しています。
[更新]
今、私は私の困難の原因を理解しています。ジェネレーターを使用しましたが、実際にはコルーチンを使用したことはありません( PEP-342 で導入されました)。いくつかの類似点はありますが、ジェネレーターとコルーチンは基本的に2つの異なる概念です。新しい構文を理解するためには、コルーチン(ジェネレーターだけでなく)を理解することが重要です。
私見コルーチンは、最もあいまいなPython機能です。ほとんどの本では、役に立たず、面白くありません。
すばらしい回答をありがとう、しかし agf と David Beazley presentations にリンクしている彼のコメントに特に感謝します。デビッドが揺れます。
最初に邪魔にならないようにしましょう。 yield from g
はfor v in g: yield v
と同等であるという説明は、yield from
が何であるかについて正義を開始することすらありません。なぜなら、yield from
がすべてfor
ループを展開することである場合、言語にyield from
を追加することを保証せず、Python 2.xで実装される新しい機能の全体を排除しないためです。
yield from
が行うことは、呼び出し元とサブジェネレーターの間の透過的な双方向接続を確立します:
接続は、生成される要素だけでなく、すべてを正しく伝播するという意味で「透過的」です(たとえば、例外が伝播されます)。
接続は、データをfromとtoジェネレーターの両方に送信できるという意味で「双方向」です。
(TCPについて話していた場合、yield from g
は「クライアントのソケットを一時的に切断し、この他のサーバーソケットに再接続する」ことを意味するかもしれません。)
ところで、ジェネレーターにデータを送信するの意味がわからない場合でも、すべてをドロップして、コルーチンについて最初に読む必要があります—これらは非常に便利ですが(subroutinesとは対照的です)、残念ながらPythonではあまり知られていません。 Dave BeazleyのCouroutinesでの好奇心の強いコース は素晴らしいスタートです。 スライド24〜33を読む クイックプライマー。
def reader():
"""A generator that fakes a read from a file, socket, etc."""
for i in range(4):
yield '<< %s' % i
def reader_wrapper(g):
# Manually iterate over data produced by reader
for v in g:
yield v
wrap = reader_wrapper(reader())
for i in wrap:
print(i)
# Result
<< 0
<< 1
<< 2
<< 3
reader()
を手動で繰り返す代わりに、単にyield from
itすることができます。
def reader_wrapper(g):
yield from g
それは機能し、1行のコードを削除しました。そして、おそらくその意図はもう少し明確(またはそうでない)です。しかし、人生は変わりません。
それでは、もっと面白いことをしましょう。 writer
という名前のコルーチンを作成して、送信されたデータを受け入れ、ソケット、fdなどに書き込みます。
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
さて、問題は、ラッパー関数がライターへのデータ送信をどのように処理する必要があるかです。そのため、ラッパーに送信されるデータは透過的にwriter()
に送信されますか?
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in range(4):
wrap.send(i)
# Expected result
>> 0
>> 1
>> 2
>> 3
ラッパーは、acceptに送信されるデータを(明らかに)受け入れる必要があり、forループが使い果たされたときにStopIteration
も処理する必要があります。明らかにfor x in coro: yield x
を実行するだけでは実行されません。これが機能するバージョンです。
def writer_wrapper(coro):
coro.send(None) # prime the coro
while True:
try:
x = (yield) # Capture the value that's sent
coro.send(x) # and pass it to the writer
except StopIteration:
pass
または、これを行うことができます。
def writer_wrapper(coro):
yield from coro
これにより、6行のコードが節約され、はるかに読みやすくなり、機能します。魔法!
もっと複雑にしましょう。ライターが例外を処理する必要がある場合はどうなりますか? writer
がSpamException
を処理し、遭遇した場合は***
を出力するとします。
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
writer_wrapper
を変更しないとどうなりますか?動作しますか?やってみよう
# writer_wrapper same as above
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
if i == 'spam':
wrap.throw(SpamException)
else:
wrap.send(i)
# Expected Result
>> 0
>> 1
>> 2
***
>> 4
# Actual Result
>> 0
>> 1
>> 2
Traceback (most recent call last):
... redacted ...
File ... in writer_wrapper
x = (yield)
__main__.SpamException
x = (yield)
は例外を発生させるだけで、すべてがクラッシュして停止するため、機能していません。動作させましょうが、例外を手動で処理して送信するか、サブジェネレーターにスローします(writer
)
def writer_wrapper(coro):
"""Works. Manually catches exceptions and throws them"""
coro.send(None) # prime the coro
while True:
try:
try:
x = (yield)
except Exception as e: # This catches the SpamException
coro.throw(e)
else:
coro.send(x)
except StopIteration:
pass
これは動作します。
# Result
>> 0
>> 1
>> 2
***
>> 4
しかし、これもそうです!
def writer_wrapper(coro):
yield from coro
yield from
は、値を送信するか、サブジェネレーターに値をスローすることを透過的に処理します。
しかし、これはまだすべてのコーナーケースをカバーしていません。外部発電機が閉じている場合はどうなりますか?サブジェネレーターが値を返す場合(はい、Python 3.3以降では、ジェネレーターは値を返すことができます)、戻り値をどのように伝播する必要がありますか? yield from
がすべてのコーナーケースを透過的に処理することは本当に印象的です 。 yield from
は魔法のように機能し、これらすべてのケースを処理します。
個人的には、yield from
はtwo-wayの性質を明らかにしないため、不適切なキーワードの選択であると感じています。提案された他のキーワードがありました(delegate
などですが、言語に新しいキーワードを追加することは既存のキーワードを組み合わせるよりもはるかに難しいため、拒否されました。
要約すると、yield from
は、呼び出し元とサブジェネレーターの間のtransparent two way channel
と考えるのが最善です。
参照:
「yield from」が役立つ状況は何ですか?
このようなループがあるすべての状況:
for x in subgenerator:
yield x
PEPが説明しているように、これはサブジェネレーターを使用するかなり単純な試みであり、いくつかの側面、特に PEP 342 によって導入された.throw()
/.send()
/.close()
メカニズムの適切な処理が欠落しています。これを適切に行うには、 やや複雑 コードが必要です。
クラシックユースケースとは何ですか?
再帰的なデータ構造から情報を抽出することを検討してください。ツリー内のすべてのリーフノードを取得するとします。
def traverse_tree(node):
if not node.children:
yield node
for child in node.children:
yield from traverse_tree(child)
さらに重要なのは、yield from
まで、ジェネレータコードをリファクタリングする簡単な方法がなかったという事実です。次のような(意味のない)ジェネレーターがあるとします。
def get_list_values(lst):
for item in lst:
yield int(item)
for item in lst:
yield str(item)
for item in lst:
yield float(item)
次に、これらのループを個別のジェネレーターに分解することにします。 yield from
がないと、実際にやりたいかどうかを二度考えてしまうまで、これは見苦しくなります。 yield from
を使用すると、実際には次のように表示できます。
def get_list_values(lst):
for sub in [get_list_values_as_int,
get_list_values_as_str,
get_list_values_as_float]:
yield from sub(lst)
マイクロスレッドと比較されるのはなぜですか?
PEPのこのセクション が言っていることは、すべてのジェネレーターが独自の分離された実行コンテキストを持っているということです。それぞれyield
および__next__()
を使用してジェネレーター-イテレーターと呼び出し元の間で実行が切り替えられるという事実と併せて、これはスレッドに似ています。オペレーティングシステムは、実行コンテキスト(スタック、レジスタ、...)。
この効果も同等です。ジェネレーター-イテレーターと呼び出し元の両方が同時に実行状態を進行し、実行がインターリーブされます。たとえば、ジェネレータが何らかの計算を行い、呼び出し元が結果を出力する場合、結果が利用可能になるとすぐに結果が表示されます。これは同時実行の形式です。
その類推はyield from
に固有のものではありませんが、Pythonのジェネレーターの一般的なプロパティです。
ジェネレーター内からジェネレーターを呼び出す場合は常に、for v in inner_generator: yield v
の値をre -yield
するための「ポンプ」が必要です。 PEPが指摘しているように、これには微妙な複雑さがあり、ほとんどの人はそれを無視しています。 throw()
のような非ローカルフロー制御は、PEPで与えられている1つの例です。新しい構文yield from inner_generator
は、以前に明示的なfor
ループを記述した場所で使用されます。ただし、単なる構文上の砂糖ではありません。for
ループによって無視されるすべてのコーナーケースを処理します。 「甘い」ということは、人々がそれを使うことを促し、正しい行動をとることを促します。
ディスカッションスレッドのこのメッセージ これらの複雑さについて説明します。
PEP 342で導入された追加のジェネレーター機能により、そうではなくなりました。GregのPEPで説明されているように、単純な反復ではsend()およびthrow()を正しくサポートしていません。 send()とthrow()をサポートするために必要な体操は、実際に分解してもそれほど複雑ではありませんが、些細なことでもありません。
ジェネレーターが並列処理の一種であることを観察する以外に、マイクロスレッドとのcomparisonに話すことはできません。中断されたジェネレータは、yield
を介してコンシューマスレッドに値を送信するスレッドと見なすことができます。実際の実装はこのようなものではないかもしれません(そして実際の実装は明らかにPython開発者にとって大きな関心事です)が、これはユーザーには関係ありません。
新しいyield from
構文は、スレッド化に関して言語に追加機能を追加するものではなく、既存の機能を正しく使用しやすくするだけです。もっと正確に言えば、初心者が、expert複雑な機能を壊さずにジェネレーターを通過させます。
短い例は、yield from
の使用例の1つを理解するのに役立ちます。別のジェネレーターから値を取得します
def flatten(sequence):
"""flatten a multi level list or something
>>> list(flatten([1, [2], 3]))
[1, 2, 3]
>>> list(flatten([1, [2], [3, [4]]]))
[1, 2, 3, 4]
"""
for element in sequence:
if hasattr(element, '__iter__'):
yield from flatten(element)
else:
yield element
print(list(flatten([1, [2], [3, [4]]])))
Asynchronous IO coroutine の使用方法では、yield from
の動作は coroutine function のawait
と似ています。どちらもコルーチンの実行を一時停止するために使用されます。
yield from
は ジェネレーターベースのコルーチン で使用されます。
Asyncioでは、古いPythonバージョン(つまり> 3.5)をサポートする必要がない場合、async def
/await
がコルーチンを定義するための推奨構文です。したがって、コルーチンではyield from
は不要になりました。
しかし、一般にasyncio以外では、yield from <sub-generator>
には、以前の回答で述べたように sub-generator を反復する他の用途があります。
yield from
は基本的に、イテレーターを効率的な方法でチェーンします。
# chain from itertools:
def chain(*iters):
for it in iters:
for item in it:
yield item
# with the new keyword
def chain(*iters):
for it in iters:
yield from it
ご覧のとおり、1つの純粋なPythonループが削除されます。それが行うことはほとんどすべてですが、反復子の連鎖はPythonでかなり一般的なパターンです。
スレッドは基本的に、完全にランダムなポイントで関数から飛び出し、別の関数の状態に戻ることができる機能です。スレッド監視プログラムはこれを非常に頻繁に行うため、プログラムはこれらの機能をすべて同時に実行するように見えます。問題は、ポイントがランダムであるため、ロックを使用して、スーパーバイザーが問題のあるポイントで機能を停止しないようにする必要があることです。
この意味で、ジェネレーターはスレッドに非常に似ています。特定のポイント(yield
の場合)を指定して、ジャンプすることができます。この方法で使用する場合、ジェネレーターはコルーチンと呼ばれます。