「降伏」という言葉には2つの意味があります。何かを生産すること(たとえば、トウモロコシを生産すること)と、誰か/何かを続けることをやめること(たとえば、歩行者に譲歩する車)。どちらの定義もPythonのyieldキーワードに適用されます。ジェネレーター関数を特別なものにするのは、通常の関数とは異なり、ジェネレーター関数を終了するのではなく一時停止するだけで、呼び出し元に値を「返す」ことができるということです。
ジェネレータを、「左」端と「右」端のある双方向パイプの一端として想像するのが最も簡単です。このパイプは、ジェネレータ自体とジェネレータ関数の本体の間で値が送信される媒体です。パイプの両端には2つの操作がありますpush。これは、値を送信し、パイプのもう一方の端が値をプルするまでブロックし、何も返しません。そしてpull、パイプの他端が値をプッシュするまでブロックし、プッシュされた値を返します。実行時、実行はパイプのいずれかの側のコンテキスト間で往復します。それぞれの側は、反対側に値を送信するまで実行され、その時点で停止し、反対側を実行させ、値を待ちます。戻り、その時点で反対側は停止し、再開します。つまり、パイプの両端は、値を受け取った瞬間から値を送信する瞬間まで続きます。
パイプは機能的に対称ですが、慣例により、この回答で定義します-左端はジェネレーター関数の本体内でのみ使用でき、yieldキーワードを介してアクセスできますが、右端はジェネレーターであり、ジェネレーターのsend機能。パイプのそれぞれの端部に特異インターフェースとして、yield及びsend二重の義務を行う:パイプのその端部から/へ、彼らはそれぞれプッシュプル値の両方、yield右方向に押すと左方向一方を引っ張るとsendは反対を行います。この二重の義務は、のようなステートメントのセマンティクスを取り巻く混乱の核心ですx = yield y。壊すyieldとsend2明示的なプッシュ/プル段階へのダウンは、その意味は、はるかに明確になります。
- 仮に
gジェネレータであるとします。g.send値をパイプの右端から左方向に押します。
g一時停止のコンテキスト内で実行し、ジェネレーター関数の本体を実行できるようにします。
- 押し込まれた値は、パイプの左端に
g.send引き寄せyieldられ、左端で受け取られます。ではx = yield y、xプルされた値に割り当てられます。
- を含む次の行に
yield到達するまで、実行はジェネレーター関数の本体内で続行されます。
yield値をパイプの左端から右方向に押し、に戻しg.sendます。ではx = yield y、yパイプを通して右方向に押されます。
- ジェネレーター関数の本体内での実行が一時停止し、外側のスコープが中断したところから続行できるようにします。
g.send 値を再開してプルし、ユーザーに返します。
g.send次にが呼び出されたら、ステップ1に戻ります。
周期的ではありますが、この手順には始まりがあります。g.send(None)それは、next(g)略して-が最初に呼び出されたときです(None最初のsend呼び出し以外のものを渡すことは違法です)。そして、それは終わりがあるかもしれません:yieldジェネレータ関数の本体で到達する必要のあるステートメントがなくなったとき。
あなたは作るものを見てくださいyield、特別なので、(またはより正確に、発電機)の文を?measly returnキーワードとyieldは異なり、は、呼び出し元に値を渡したり、呼び出し元から値を受け取ったりすることができます。(もちろん、関数またはジェネレーターを終了したい場合は、returnキーワードも持つと便利です。)yieldステートメントが検出された場合、ジェネレーター関数は単に一時停止し、左の場所に戻ります。別の値が送信されるとオフになります。そして、それsendは外部からジェネレータ関数の内部と通信するためのインターフェースです。
私たちは本当にダウン限り我々はできるように、このプッシュ/プル/パイプアナロジーを破るしたい場合は、私たちは本当に家を駆動することを次の擬似コードで終わり、それはさておき、ステップ1-5からyieldとsend同じの両側あるコインパイプ:
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
キー変換は、我々が分割されていることであるx = yield yとvalue1 = 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次の通常の(非ジェネレーター)関数に対する大まかなデスガーの定義:
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。
- 実装を入れ子関数に移動しました。
left_endネストされた関数right_endによってアクセスされ、外側のスコープによって返されてアクセスされる双方向パイプを作成しました-これright_endがジェネレーターオブジェクトとして知られているものです。
- ネストされた関数内で最初に行うこと
left_end.pull()はNone、チェック、つまり、プロセスでプッシュされた値を消費することです。
- ネストされた関数内で、ステートメント
x = yield yは2行に置き換えられました:left_end.push(y)およびx = left_end.pull()。
- の
send関数を定義しましたright_end。これはx = yield y、前の手順でステートメントを置き換えた2行に相当します。
関数が戻った後に継続することができ、このファンタジーの世界では、g割り当てられright_endた後、impl()呼ばれています。したがって、上記の例では、実行を1行ずつ追跡すると、おおよそ次のようになります。
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
ほとんどの場合、それらはと同じ方法で砂糖を取り除き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ため、UnboundLocalErrorが発生します。