「yield」という言葉には 2 つの意味があります。何かを生産すること (例: トウモロコシを収穫する) と、停止して誰か/何かを継続させること (例: 車が歩行者に道を譲る)。どちらの定義も Python のyieldキーワードに適用されます。ジェネレーター関数を特別なものにしているのは、通常の関数とは異なり、ジェネレーター関数を終了するのではなく一時停止するだけで、呼び出し元に値を「返す」ことができることです。
ジェネレーターは、「左」端と「右」端を持つ双方向パイプの一端として想像するのが最も簡単です。このパイプは、ジェネレーター自体とジェネレーター関数の本体の間で値が送信される媒体です。パイプの両端には 2 つの操作pushがあります。 は値を送信し、パイプのもう一方の端が値をプルして何も返さないまでブロックします。とpullパイプのもう一方の端が値をプッシュするまでブロックし、プッシュされた値を返します。実行時には、実行はパイプの両側のコンテキスト間を行ったり来たりします。それぞれの側は、反対側に値を送信するまで実行されます。値が送信された時点で停止し、反対側を実行させ、値が入力されるのを待ちます。戻り、その時点で反対側が停止し、再開します。つまり、パイプの両端は、値を受信した瞬間から値を送信する瞬間まで実行されます。
パイプは機能的に対称ですが、慣例により、この回答で定義しています-左端はジェネレーター関数の本体内でのみ使用でき、yieldキーワードを介してアクセスできますが、右端はジェネレーターであり、ジェネレーターのsend機能。パイプのそれぞれの端への単一のインターフェースとして、yield二send重の役割を果たします。それぞれ、パイプの端との間で値をプッシュおよびプルし、yield右にプッシュして左にプルsendし、反対のことを行います。この二重の義務は、 のようなステートメントのセマンティクスを取り巻く混乱の核心ですx = yield y。yield2 つの明示的なプッシュ/プル ステップに分解して分解すると、セマンティクスsendがより明確になります。
- がジェネレーターである
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)以外に何かを渡すことは違法です)。そして、それには終わりがあるかもしれません:ジェネレーター関数の本体に到達するステートメントがなくなったときです。Nonesendyield
yieldステートメント (より正確には、ジェネレーター) が特別な理由がわかりますか? measlyreturnキーワードとyieldは異なり、関数を終了することなく、呼び出し元に値を渡したり、呼び出し元から値を受け取ったりすることができます。(もちろん、関数 (またはジェネレーター) を終了したい場合は、returnキーワードも指定すると便利です。)yieldステートメントに遭遇すると、ジェネレーター関数は一時停止するだけで、終了した場所に戻ります。別の値が送信されるとオフになります。Andsendは、ジェネレーター関数の外部からジェネレーター関数の内部と通信するための単なるインターフェイスです。
このプッシュ/プル/パイプのアナロジーをできる限り分解したい場合は、ステップ 1 ~ 5 を除いて、同じコインパイプの 2 つの側面であることを実際に理解させる次の疑似コードに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
重要な変換は、and を 2 つのステートメントに分割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おおよその 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:
- 実装をネストされた関数に移動しました。
left_endネストされた関数によってright_endアクセスされ、外側のスコープによって返されてアクセスされる双方向パイプを作成しました。これright_endは、ジェネレータ オブジェクトとして知られているものです。
- ネストされた関数内で最初に行うことは
left_end.pull()、Noneプロセスでプッシュされた値を消費することです。
- ネストされた関数内では、ステートメントがとの
x = yield y2 行に置き換えられています。left_end.push(y)x = left_end.pull()
send関数 forを定義しました。これは、前の手順でステートメントright_endを置き換えた 2 行に相当します。x = yield y
帰還後も機能を継続できるこのファンタジー世界でgは、割り当てられright_endてimpl()呼び出されます。上記の例で、行ごとに実行を追跡すると、おおよそ次のようになります。
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が上げられます。