web-dev-qa-db-ja.com

実際には、Python 3.3の新しい「yield from」構文の主な用途は何ですか?

PEP 38 で頭を包むのに苦労しています。

  1. 「yield from」が役立つ状況は何ですか?
  2. クラシックユースケースとは何ですか?
  3. マイクロスレッドと比較されるのはなぜですか?

[更新]

今、私は私の困難の原因を理解しています。ジェネレーターを使用しましたが、実際にはコルーチンを使用したことはありません( PEP-342 で導入されました)。いくつかの類似点はありますが、ジェネレーターとコルーチンは基本的に2つの異なる概念です。新しい構文を理解するためには、コルーチン(ジェネレーターだけでなく)を理解することが重要です。

私見コルーチンは、最もあいまいなPython機能です。ほとんどの本では、役に立たず、面白くありません。

すばらしい回答をありがとう、しかし agfDavid Beazley presentations にリンクしている彼のコメントに特に感謝します。デビッドが揺れます。

312
Paulo Scardine

最初に邪魔にならないようにしましょう。 yield from gfor v in g: yield vと同等であるという説明は、yield fromが何であるかについて正義を開始することすらありません。なぜなら、yield fromがすべてforループを展開することである場合、言語にyield fromを追加することを保証せず、Python 2.xで実装される新しい機能の全体を排除しないためです。

yield fromが行うことは、呼び出し元とサブジェネレーターの間の透過的な双方向接続を確立します

  • 接続は、生成される要素だけでなく、すべてを正しく伝播するという意味で「透過的」です(たとえば、例外が伝播されます)。

  • 接続は、データをfromtoジェネレーターの両方に送信できるという意味で「双方向」です。

TCPについて話していた場合、yield from gは「クライアントのソケットを一時的に切断し、この他のサーバーソケットに再接続する」ことを意味するかもしれません。

ところで、ジェネレーターにデータを送信するの意味がわからない場合でも、すべてをドロップして、コルーチンについて最初に読む必要があります—これらは非常に便利ですが(subroutinesとは対照的です)、残念ながらPythonではあまり知られていません。 Dave BeazleyのCouroutinesでの好奇心の強いコース は素晴らしいスタートです。 スライド24〜33を読む クイックプライマー。

Yield fromを使用してジェネレーターからデータを読み取る

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行のコードを削除しました。そして、おそらくその意図はもう少し明確(またはそうでない)です。しかし、人生は変わりません。

Yield fromを使用してジェネレーター(コルーチン)にデータを送信する-パート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行のコードが節約され、はるかに読みやすくなり、機能します。魔法!

ジェネレーターにデータを送信する-第2部-例外処理

もっと複雑にしましょう。ライターが例外を処理する必要がある場合はどうなりますか? writerSpamExceptionを処理し、遭遇した場合は***を出力するとします。

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 fromtwo-wayの性質を明らかにしないため、不適切なキーワードの選択であると感じています。提案された他のキーワードがありました(delegateなどですが、言語に新しいキーワードを追加することは既存のキーワードを組み合わせるよりもはるかに難しいため、拒否されました。

要約すると、yield fromは、呼び出し元とサブジェネレーターの間のtransparent two way channelと考えるのが最善です。

参照:

  1. PEP 38 -サブジェネレーターに委任するための構文(Ewing)[v3.3、2009-02-13]
  2. PEP 342 -拡張ジェネレーターを介したコルーチン(GvR、Eby)[v2.5、2005-05-10]
450

「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のジェネレーターの一般的なプロパティです。

77
Niklas B.

ジェネレーター内からジェネレーターを呼び出す場合は常に、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複雑な機能を壊さずにジェネレーターを通過させます。

28
Ben Jackson

短い例は、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]]])))
19
ospider

Asynchronous IO coroutine の使用方法では、yield fromの動作は coroutine functionawaitと似ています。どちらもコルーチンの実行を一時停止するために使用されます。

Asyncioでは、古いPythonバージョン(つまり> 3.5)をサポートする必要がない場合、async def/awaitがコルーチンを定義するための推奨構文です。したがって、コルーチンではyield fromは不要になりました。

しかし、一般にasyncio以外では、yield from <sub-generator>には、以前の回答で述べたように sub-generator を反復する他の用途があります。

3
Yeo

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の場合)を指定して、ジャンプすることができます。この方法で使用する場合、ジェネレーターはコルーチンと呼ばれます。

詳細については、Pythonのコルーチンに関するこの優れたチュートリアルを参照してください

3
Jochen Ritzel