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


407

PEP 380頭を悩ませるのに苦労しています。

  1. 「yield from」が役立つ状況は何ですか?
  2. 従来の使用例は何ですか?
  3. なぜそれがマイクロスレッドと比較されるのですか?

【更新】

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

私見コルーチンは最も不明瞭なPython機能であり、ほとんどの本はそれを役に立たなくて興味がないように見せています。

素晴らしい回答に感謝しますが、agfDavid Beazleyのプレゼンテーションにリンクしている彼のコメントに特に感謝します。デビッド・ロックス。



7
David Beazleyのdabeaz.com/coroutinesプレゼンテーションの動画:youtube.com/watch
v

回答:


571

最初に、邪魔にならないものを1つ取得しましょう。にyield from g相当する説明は、すべてが何であるかについてfor v in g: yield v 正当化さえませんyield from。正直なところ、ループをyield from拡張するだけの場合は、言語にfor追加yield fromすることを保証するものではなく、Python 2.xに実装されている新機能の全体を排除するものではありません。

yield from行うことは、それが発信者とサブジェネレータの間に透明の双方向接続を確立します

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

  • 接続データは、両方の送信することができるという意味で「双方向」であるからする発電機。

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

ところで、ジェネレータにデータを送信する意味さえわからない場合は、まずすべてを削除し、コルーチンについて読む必要があります。コルーチンは非常に便利ですが(サブルーチンと比較してください)、残念ながらPythonではあまり知られていません。デイブビーズリーのコルーチンに関する好奇心旺盛なコースは素晴らしいスタートです。クイックプライマーについては、スライド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ます。

def reader_wrapper(g):
    yield from g

これでうまくいき、コードを1行削除しました。そしておそらくその意図は少し明確です(またはそうではありません)。しかし、人生は変わりません。

収量を使用してジェネレーター(コルーチン)にデータを送信する-パート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

ラッパーは(当然のことながら)送信されたデータを受け入れる必要がありStopIteration、forループが使い果たされたときにも処理する必要があります。明らかに、ただやるだけで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-例外処理

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

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

要約すると、それは考えるのがベストだyield fromとしてtransparent two way channel、発信者とサブジェネレータの間。

参照:

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

3
@PraveenGollakota、質問の2番目の部分である、yield from- パート1を使用してジェネレーター(コルーチン)にデータを送信する場合、受信したアイテムを転送するコルーチンが複数ある場合はどうなりますか?あなたの例でラッパーに複数のコルーチンを提供し、アイテムをそれらのすべてまたはサブセットに送信する必要がある放送局またはサブスクライバーのシナリオのように?
Kevin Ghaboosi 2016年

3
@PraveenGollakota、素晴らしい答えの称賛。小さな例では、replで物事を試すことができます。Dave Beazleyコースへのリンクはボーナスでした!
BiGYaN 2016年

1
ループを内側で行うとexcept StopIteration: passwhile True:ループが正確に表現されyield from coroません。これは無限ループではなく、coro使い尽くされた後(つまり、StopIterationが発生)、writer_wrapper次のステートメントが実行されます。最後のステートメントの後、それ自体StopIterationは使い尽くされた発電機として自動上昇します...
Aprillion '

1
...したがって、の代わりにwriter含まれている場合for _ in range(4)while True、印刷後に>> 3自動レイズされStopIteration、これが自動ハンドルされyield from、それwriter_wrapper自体が自動レイズされます。また、がブロック内にないStopIterationため、この時点で実際にレイズされます(つまり、トレースバックは、ジェネレーター内からのものではなく、の行のみを報告します)wrap.send(i)trywrap.send(i)
Aprillion

3
正義さえも始まらない」を読んだとき、私は正しい答えに到達したことを知っています。素晴らしい説明ありがとうございます!
Hot.PxL 2017年

89

「yield from」が役立つ状況は何ですか?

このようなループがあるすべての状況:

for x in subgenerator:
  yield x

PEPは説明したように、これはsubgeneratorを使用してではなく、素朴な試みである、それはいくつかの側面、特に適切な取扱い欠けている.throw()/ .send()/ .close()によって導入メカニズムPEP 342。これを適切に行うには、かなり複雑なコードが必要です。

従来の使用例は何ですか?

再帰的なデータ構造から情報を抽出することを検討してください。ツリーのすべての葉ノードを取得したいとしましょう:

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


発電機をリファクタリングすることで痛みを伴う今日。
Josh Lee

1
私はジェネレーター(itertools.chainのようなもの)をリファクタリングするためにitertoolsを頻繁に使用する傾向がありますが、それほど大したことではありません。私は収量が好きですが、それがどれほど革新的であるかはまだわかりません。グイドは夢中になっているので、おそらくそうですが、全体像を見逃しているに違いありません。これはリファクタリングが難しいため、send()には最適だと思いますが、あまり頻繁には使用しません。
e-satis 2013年

私はそれらget_list_values_as_xxxが単一のラインfor x in input_param: yield int(x)を持つ単純なジェネレーターであり、他の2つはそれぞれstrfloat
madtynを

@NiklasB。re「再帰的なデータ構造から情報を抽出する」。私はPyのデータを取得しています。このQで刺していただけますか?
アランカルヴィッティ

33

ジェネレータ内からジェネレータを呼び出す場所はどこでもyield、値を再設定するための「ポンプ」が必要です for v in inner_generator: yield v。PEPが指摘するように、これには微妙な複雑さがあり、ほとんどの人は無視します。非ローカルフロー制御のようなものthrow()は、PEPで与えられた1つの例です。新しい構文yield from inner_generatorは、for以前に明示的なループを記述した場合に使用されます。ただし、これは単なる構文上の砂糖ではありませんfor。ループによって無視されるすべてのコーナーケースを処理します。「砂糖漬け」であることは人々にそれを使用することを奨励し、それによって正しい振る舞いを得ます。

ディスカッションスレッドのこのメッセージは、これらの複雑さについて説明しています

PEP 342で導入された追加のジェネレーター機能により、これはもはや当てはまりません。GregのPEPで説明されているように、単純な反復ではsend()とthrow()が正しくサポートされません。send()とthrow()をサポートするために必要な体操は、実際に分解してもそれほど複雑ではありませんが、簡単ではありません。

ジェネレーターが一種の並列性であることを観察する以外に、マイクロスレッドとの比較について話すことはできません。中断されたジェネレーターyieldは、コンシューマースレッドに値を送信するスレッドと見なすことができます。実際の実装はこのようなものではない可能性があります(実際の実装は明らかにPython開発者にとって大きな関心事です)が、これはユーザーには関係ありません。

新しいyield from構文は、スレッドに関して言語に追加機能を追加するものではなく、既存の機能を正しく使用することを容易にするだけです。または、より正確には、専門家が作成した複雑な内部ジェネレーターの初心者が、複雑な機能を損なうことなくそのジェネレーターを簡単に通過できるようにします。


23

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

2
末尾の印刷がリストに変換しなくても少し見栄えが良くなることを提案したかっただけです–print(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

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のコルーチンに関する優れたチュートリアルをお読みください。


10
この回答は、上で述べたように、「yield from」の顕著な特徴であるsend()とthrow()のサポートを省略しているため、誤解を招く可能性があります。
ジャスティンW

2
@Justin W:コードを簡略化するために適切に実装する必要throw()/send()/close()があるyield機能であるという点が理解できなかったため、以前に読んだものはすべて誤解を招くと思いyield fromます。そのような些細なことは、使用法とは何の関係もありません。
Jochen Ritzel 2013

5
上記のベンジャクソンの答えに異議を唱えていますか?あなたの答えの私の読書は、それがあなたが提供したコード変換に従う本質的に構文糖であるということです。ベン・ジャクソンの答えは、特にその主張に反駁しています。
ジャスティンW

@JochenRitzel すでに存在するchainため、独自の関数を記述する必要はありませんitertools.chain。を使用しyield from itertools.chain(*iters)ます。
アキュメニュ

4

適用される用法で非同期IOのコルーチンyield from同様の挙動を有するawaitコルーチン機能。どちらもコルーチンの実行を一時停止するために使用されます。

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

しかし、一般にasyncioの外では、前の回答で述べたように、サブジェネレーターyield from <sub-generator>繰り返す際に他の使用法があります。


1

このコードは、fixed_sum_digits数字の合計が20になるように、6桁の数字をすべて列挙するジェネレータを返す関数を定義します。

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

なしで書いてみてくださいyield from。効果的な方法を見つけた場合はお知らせください。

このような場合、ツリーを訪問yield fromすることで、コードがよりシンプルでクリーンになると思います。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.