211

Pythonジェネレーター関数に関連付けられた「送信」関数が存在する理由の例を誰か教えてもらえますか? 利回り関数を完全に理解しています。ただし、送信機能は私にはわかりにくいです。このメソッドに関するドキュメントは複雑です。

generator.send(value)

実行を再開し、値をジェネレーター関数に「送信」します。value 引数は、現在の yield 式の結果になります。send() メソッドは、ジェネレーターによって生成された次の値を返すか、ジェネレーターが別の値を生成せずに終了した場合は StopIteration を発生させます。

どういう意味ですか?値は関数への入力だと思いましたか? 「send() メソッドは、ジェネレーターによって生成された次の値を返します」というフレーズも、yield 関数の正確な目的のようです。yield は、ジェネレーターによって生成された次の値を返します...

誰かが send を利用して yield できない何かを達成するジェネレーターの例を教えてもらえますか?

4

9 に答える 9

203

生成されたばかりのジェネレーターに値を送信するために使用されます。以下は、人為的な (役に立たない) 説明の例です。

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

だけではできませんyield

なぜ便利なのかというと、私が見た中で最も優れた使用例の 1 つは Twisted の@defer.inlineCallbacks. 基本的に、次のような関数を書くことができます:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

これは、値が後で計算されることを約束する値ですtakesTwoSeconds()DeferredTwisted は別のスレッドで計算を実行できます。計算が完了すると、それが deferred に渡され、値がdoStuff()関数に返されます。したがって、doStuff()は、あらゆる種類の計算やコールバックなどを実行できることを除いて、多かれ少なかれ通常の手続き型関数のように見えることになります。この機能の前の代替手段は、次のようなことを行うことです。

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

それははるかに複雑で扱いにくいです。

于 2013-10-10T17:47:01.287 に答える
126

この関数はコルーチンを書くためのものです

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

版画

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

コントロールがどのようにやり取りされているかがわかりますか? それらはコルーチンです。これらは、非同期 IO などのあらゆる種類の優れた機能に使用できます。

このように考えてください。ジェネレーターがあり、送信されていない場合、一方通行です。

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

しかし、送信すると、双方向になります

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

これにより、ユーザーがオンザフライでジェネレーターの動作をカスタマイズし、ジェネレーターがユーザーに応答するようになります。

于 2013-10-10T17:47:32.647 に答える
73

これは誰かを助けるかもしれません。これは send 関数の影響を受けないジェネレータです。インスタンス化時に数値パラメーターを受け取り、送信の影響を受けません。

>>> def double_number(number):
...     while True:
...         number *=2 
...         yield number
... 
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

次に、send を使用して同じタイプの関数を実行する方法を示します。そのため、各反復で number の値を変更できます。

def double_number(number):
    while True:
        number *= 2
        number = yield number

number に新しい値を送信すると結果が変わることがわかるように、次のようになります。

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>> 
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

これを次のように for ループに入れることもできます。

for x in range(10):
    n = c.send(n)
    print n

詳細については、このすばらしいチュートリアルをご覧ください。

于 2014-10-06T13:48:50.753 に答える
22

ジェネレーターとsend()

send()許可のあるジェネレーター:

  • 実行の内部状態を記憶する
    • 私たちはどの段階にいるのか
    • 私たちのデータの現在の状態は何ですか
  • 値のシーケンスを返す
  • 入力の受信シーケンス

いくつかの使用例を次に示します。

レシピに従う試みを見た

あらかじめ定義された一連の入力をある順序で期待するレシピを考えてみましょう。

私たちは:

  • watched_attemptレシピからインスタンスを作成する
  • いくつかの入力を取得させます
  • 各入力で、現在ポットにあるものに関する情報を返します
  • 各入力チェックで、入力が期待どおりであることを確認します (そうでない場合は失敗します)。

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot
    

これを使用するには、まずwatched_attemptインスタンスを作成します。

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     

.next()ジェネレーターの実行を開始するには、への呼び出しが必要です。

返された値は、ポットが現在空であることを示しています。

レシピが期待するものに従って、いくつかのアクションを実行します。

>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "salt"))                                                                      
['water', 'salt']                                                                                      
>>> watched_attempt.send(("boil", "water"))                                                                    
['water', 'salt']                                                                                      
>>> watched_attempt.send(("add", "pasta"))                                                                     
['water', 'salt', 'pasta']                                                                             
>>> watched_attempt.send(("decant", "water"))                                                                  
['salt', 'pasta']                                                                                      
>>> watched_attempt.send(("serve"))                                                                            
[] 

ご覧のとおり、ポットはついに空になりました。

レシピに従わなかった場合、それは失敗します (何かを料理しようとするのを見て、望ましい結果になる可能性があります - 指示を受けたときに十分な注意を払っていなかったことを知っただけです.

>>> watched_attempt = running.recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "pasta")) 

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

次のことに注意してください。

  • 期待されるステップの線形シーケンスがあります
  • 手順は異なる場合があります(一部は削除され、一部はポットに追加されます)
  • 関数/ジェネレーターによってすべてを行うことができます-複雑なクラスや同様の構造を使用する必要はありません。

累計

ジェネレーターを使用して、送信された値の現在の合計を追跡する場合があります。

数値を追加するたびに、入力の数と合計が返されます (前の入力が送信された瞬間に有効です)。

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

出力は次のようになります。

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
于 2016-04-26T14:40:53.230 に答える
16

sendメソッドはコルーチンを実装します

コルーチンに遭遇したことがない場合、コルーチンはプログラムの流れを変えるため、理解するのが難しいでしょう。詳細については、優れたチュートリアルを読むことができます。

于 2013-10-10T17:45:11.503 に答える
10

「yield」という言葉には 2 つの意味があります。何かを生産すること (例: トウモロコシを収穫する) と、停止して誰か/何かを継続させること (例: 車が歩行者に道を譲る)。どちらの定義も Python のyieldキーワードに適用されます。ジェネレーター関数を特別なものにしているのは、通常の関数とは異なり、ジェネレーター関数を終了するのではなく一時停止するだけで、呼び出し元に値を「返す」ことができることです。

ジェネレーターは、「左」端と「右」端を持つ双方向パイプの一端として想像するのが最も簡単です。このパイプは、ジェネレーター自体とジェネレーター関数の本体の間で値が送信される媒体です。パイプの両端には 2 つの操作pushがあります。 は値を送信し、パイプのもう一方の端が値をプルして何も返さないまでブロックします。とpullパイプのもう一方の端が値をプッシュするまでブロックし、プッシュされた値を返します。実行時には、実行はパイプの両側のコンテキスト間を行ったり来たりします。それぞれの側は、反対側に値を送​​信するまで実行されます。値が送信された時点で停止し、反対側を実行させ、値が入力されるのを待ちます。戻り、その時点で反対側が停止し、再開します。つまり、パイプの両端は、値を受信した瞬間から値を送信する瞬間まで実行されます。

パイプは機能的に対称ですが、慣例により、この回答で定義しています-左端はジェネレーター関数の本体内でのみ使用でき、yieldキーワードを介してアクセスできますが、右端ジェネレーターであり、ジェネレーターのsend機能。パイプのそれぞれの端への単一のインターフェースとして、yieldsend重の役割を果たします。それぞれ、パイプの端との間で値をプッシュおよびプルし、yield右にプッシュして左にプルsendし、反対のことを行います。この二重の義務は、 のようなステートメントのセマンティクスを取り巻く混乱の核心ですx = yield yyield2 つの明示的なプッシュ/プル ステップに分解して分解すると、セマンティクスsendがより明確になります。

  1. がジェネレーターであるgとします。g.sendパイプの右端から値を左にプッシュします。
  2. 一時停止のコンテキスト内で実行しg、ジェネレーター関数の本体を実行できるようにします。
  3. によってプッシュされた値は、パイプの左端でg.send左にプルされ、受信されます。yieldではx = yield yxプルされた値に割り当てられます。
  4. yield次の行に到達するまで、ジェネレーター関数の本体内で実行が続行されます。
  5. yield値をパイプの左端から右にプッシュし、 に戻しg.sendます。ではx = yield yyパイプを通って右方向に押されます。
  6. ジェネレーター関数の本体内での実行が一時停止し、外側のスコープが中断したところから続行できるようになります。
  7. g.send値を再開してプルし、ユーザーに返します。
  8. 次に が呼び出されたらg.send、手順 1 に戻ります。

周期的ではありますが、この手順には開始点があります: g.send(None)--の略である -- が最初に呼び出されたとき (最初の呼び出しnext(g)以外に何かを渡すことは違法です)。そして、それには終わりがあるかもしれません:ジェネレーター関数の本体に到達するステートメントがなくなったときです。Nonesendyield

yieldステートメント (より正確には、ジェネレーター) が特別な理由がわかりますか? measlyreturnキーワードとyieldは異なり、関数を終了することなく、呼び出し元に値を渡したり、呼び出し元から値を受け取ったりすることができます。(もちろん、関数 (またはジェネレーター) を終了したい場合は、returnキーワードも指定すると便利です。)yieldステートメントに遭遇すると、ジェネレーター関数は一時停止するだけで、終了した場所に戻ります。別の値が送信されるとオフになります。Andsendは、ジェネレーター関数の外部からジェネレーター関数の内部と通信するための単なるインターフェイスです。

このプッシュ/プル/パイプのアナロジーをできる限り分解したい場合は、ステップ 1 ~ 5 を除いて、同じコインパイプの 2 つの側面であることを実際に理解させる次の疑似コードにyieldなります。send

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. goto 6

重要な変換は、and を 2 つのステートメントに分割x = yield yvalue1 = g.send(value2)left_end.push(y)ことx = left_end.pull()です。とvalue1 = right_end.pull()right_end.push(value2). yieldキーワードには、 と の 2 つの特殊なケースがありx = yieldますyield y。これらは、それぞれ と のシンタックス シュガーx = yield Noneです_ = yield y # discarding value

値がパイプを介して送信される正確な順序に関する具体的な詳細については、以下を参照してください。


以下は、上記のかなり長い具体的なモデルです。gまず、任意のジェネレーターについて、next(g)は とまったく同じであることに注意してくださいg.send(None)。これを念頭に置いて、どのように機能するかのみに焦点を当て、 をsend使用してジェネレーターを進めることについてのみ話すことができますsend

私たちが持っているとしましょう

def f(y):  # This is the "generator function" referenced above
    while True:
        x = yield y
        y = x
g = f(1)
g.send(None)  # yields 1
g.send(2)     # yields 2

ここで、fおおよその desugar の定義は、次の通常の (非ジェネレーター) 関数になります。

def f(y):
    bidirectional_pipe = BidirectionalPipe()
    left_end = bidirectional_pipe.left_end
    right_end = bidirectional_pipe.right_end

    def impl():
        initial_value = left_end.pull()
        if initial_value is not None:
            raise TypeError(
                "can't send non-None value to a just-started generator"
            )

        while True:
            left_end.push(y)
            x = left_end.pull()
            y = x

    def send(value):
        right_end.push(value)
        return right_end.pull()

    right_end.send = send

    # This isn't real Python; normally, returning exits the function. But
    # pretend that it's possible to return a value from a function and then
    # continue execution -- this is exactly the problem that generators were
    # designed to solve!
    return right_end
    impl()

のこの変換で次のことが起こりましたf:

  1. 実装をネストされた関数に移動しました。
  2. left_endネストされた関数によってright_endアクセスされ、外側のスコープによって返されてアクセスされる双方向パイプを作成しました。これright_endは、ジェネレータ オブジェクトとして知られているものです。
  3. ネストされた関数内で最初に行うことはleft_end.pull()Noneプロセスでプッシュされた値を消費することです。
  4. ネストされた関数内では、ステートメントがとのx = yield y2 行に置き換えられています。left_end.push(y)x = left_end.pull()
  5. send関数 forを定義しました。これは、前の手順でステートメントright_endを置き換えた 2 行に相当します。x = yield y

帰還後も機能を継続できるこのファンタジー世界でgは、割り当てられright_endimpl()呼び出されます。上記の例で、行ごとに実行を追跡すると、おおよそ次のようになります。

left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end

y = 1  # from g = f(1)

# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks

# Receive the pushed value, None
initial_value = left_end.pull()

if initial_value is not None:  # ok, `g` sent None
    raise TypeError(
        "can't send non-None value to a just-started generator"
    )

left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off

# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()

# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes

# Receive the pushed value, 2
x = left_end.pull()
y = x  # y == x == 2

left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off

# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()

x = left_end.pull()
# blocks until the next call to g.send

これは、上記の 16 ステップの疑似コードに正確に対応します。

エラーがどのように伝播されるか、ジェネレーターの最後に到達したとき (パイプが閉じられたとき) に何が起こるかなど、他にもいくつかの詳細がありますが、これにより、 が使用されたときに基本的な制御フローがどのように機能するかが明確になりますsend

これらの同じ脱糖ルールを使用して、2 つの特殊なケースを見てみましょう。

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x

ほとんどの場合、それらは と同じ方法で desugar しfます。唯一の違いは、yieldステートメントがどのように変換されるかです。

def f1(x):
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end


def f2():
    # ... set up pipe

    def impl():
        # ... check that initial sent value is None

        while True:
            left_end.push(x)
            x = left_end.pull()

    # ... set up right_end

最初に、渡された値が最初にf1プッシュ (生成) され、次に、プル (送信) されたすべての値がすぐにプッシュ (生成) されます。2 番目の では、x最初に に来たときに (まだ) 値がないpushため、 anUnboundLocalErrorが上げられます。

于 2020-02-03T15:21:43.757 に答える