“yield”这个词有两个含义:生产某物(例如,生产玉米),以及停下来让其他人/事物继续(例如,汽车让行人)。两个定义都适用于 Python 的yield
关键字;生成器函数的特别之处在于,与常规函数不同,值可以“返回”给调用者,而只是暂停而不是终止生成器函数。
最容易将发电机想象为具有“左”端和“右”端的双向管道的一端;这个管道是在生成器本身和生成器函数的主体之间发送值的媒介。管道的每一端都有两个操作:push
,它发送一个值并阻塞,直到管道的另一端拉取值,并且什么也不返回;和pull
,它阻塞直到管道的另一端推送一个值,并返回推送的值。在运行时,执行在管道任一侧的上下文之间来回反弹——每一侧运行直到它向另一侧发送一个值,此时它停止,让另一侧运行,并等待一个值返回,此时另一侧停止并恢复。换句话说,管道的每一端都是从它接收到一个值到它发送一个值的那一刻。
管道在功能上是对称的,但是——按照我在这个答案中定义的约定——左端只能在生成器函数体内使用,并且可以通过yield
关键字访问,而右端是生成器,可以通过发电机的send
功能。作为管道各自端的单一接口,yield
并send
执行双重职责:它们都向/从管道的两端推和拉值,yield
向右推和向左拉,而send
相反。这种双重职责是围绕语句语义混淆的症结,如x = yield y
. 分解yield
成send
两个显式的推/拉步骤将使它们的语义更加清晰:
- 假设
g
是发电机。g.send
通过管道的右端向左推动一个值。
- 在暂停上下文中执行
g
,允许生成器函数的主体运行。
- 推入的值
g.send
被向左拉,yield
并在管道的左端接收。在x = yield y
中,x
分配给拉取的值。
- 在生成器函数的主体内继续执行,直到到达下一行
yield
。
yield
通过管道的左端向右推动一个值,回到g.send
. In x = yield y
,y
被向右推通过管道。
- 生成器函数体内的执行暂停,允许外部作用域从它停止的地方继续。
g.send
恢复并拉取值并将其返回给用户。
- 下次调用时
g.send
,返回步骤 1。
虽然是循环的,但这个过程确实有一个开始:当g.send(None)
-- 这是什么的缩写 -- 第一次被调用(除了第一次调用next(g)
之外传递其他东西是非法的)。它可能有一个结束:当生成器函数的主体中没有更多的语句可以到达时。None
send
yield
你看到是什么让yield
语句(或更准确地说,生成器)如此特别吗?与 measlyreturn
关键字不同,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
and value1 = g.send(value2)
each 拆分为两个语句:left_end.push(y)
and x = left_end.pull()
; 和value1 = right_end.pull()
和right_end.push(value2)
。yield
关键字有两种特殊情况:x = yield
和yield y
。它们分别是 forx = 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
已替换为两行:left_end.push(y)
和x = left_end.pull()
.
- 我们已经定义了
send
for 函数,它与我们在上一步中替换语句right_end
的两行相对应。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
使用时是如何工作的。
使用这些相同的脱糖规则,让我们看两个特殊情况:
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
最初被推送(产生),然后所有拉出(发送)的值都被立即推送(产生)。在第二个中,x
当它第一次出现时没有价值(还)push
,所以 anUnboundLocalError
被提出。