“屈服”一词有两个含义:生产某种东西(例如,生产玉米),以及停下来让某人/其他东西继续(例如,汽车向行人屈服)。两种定义都适用于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。在中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关键字有两种特殊情况: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已替换为两行:left_end.push(y)和x = left_end.pull()。
- 我们已经为定义了
send函数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,因此UnboundLocalError引发。