「yield」という言葉には 2 つの意味があります。何かを生産すること (例: トウモロコシを収穫する) と、停止して誰か/何かを継続させること (例: 車が歩行者に道を譲る)。どちらの定義も Python のyield
キーワードに適用されます。ジェネレーター関数を特別なものにしているのは、通常の関数とは異なり、ジェネレーター関数を終了するのではなく一時停止するだけで、呼び出し元に値を「返す」ことができることです。
ジェネレーターは、「左」端と「右」端を持つ双方向パイプの一端として想像するのが最も簡単です。このパイプは、ジェネレーター自体とジェネレーター関数の本体の間で値が送信される媒体です。パイプの両端には 2 つの操作push
があります。 は値を送信し、パイプのもう一方の端が値をプルして何も返さないまでブロックします。とpull
パイプのもう一方の端が値をプッシュするまでブロックし、プッシュされた値を返します。実行時には、実行はパイプの両側のコンテキスト間を行ったり来たりします。それぞれの側は、反対側に値を送信するまで実行されます。値が送信された時点で停止し、反対側を実行させ、値が入力されるのを待ちます。戻り、その時点で反対側が停止し、再開します。つまり、パイプの両端は、値を受信した瞬間から値を送信する瞬間まで実行されます。
パイプは機能的に対称ですが、慣例により、この回答で定義しています-左端はジェネレーター関数の本体内でのみ使用でき、yield
キーワードを介してアクセスできますが、右端はジェネレーターであり、ジェネレーターのsend
機能。パイプのそれぞれの端への単一のインターフェースとして、yield
二send
重の役割を果たします。それぞれ、パイプの端との間で値をプッシュおよびプルし、yield
右にプッシュして左にプルsend
し、反対のことを行います。この二重の義務は、 のようなステートメントのセマンティクスを取り巻く混乱の核心ですx = yield y
。yield
2 つの明示的なプッシュ/プル ステップに分解して分解すると、セマンティクス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)
以外に何かを渡すことは違法です)。そして、それには終わりがあるかもしれません:ジェネレーター関数の本体に到達するステートメントがなくなったときです。None
send
yield
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 y
2 行に置き換えられています。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
が上げられます。