我了解yield
。但是生成器的send
函数是做什么的呢?文档中写道:
恢复执行并向生成器函数“发送”一个值。`value` 参数成为当前 `yield` 表达式的结果。`send()` 方法返回生成器产出的下一个值,如果生成器未产出其他值则引发 `StopIteration` 异常。请问是否有使用 `send` 的生成器示例,可以实现 `yield` 无法完成的功能?generator.send(value)
它用于将值发送到刚刚yielded的生成器中。以下是一个人造(不实用)的解释性示例:
>>> def double_inputs():
... while True:
... x = yield
... yield x * 2
...
>>> gen = double_inputs()
>>> next(gen) # run up to the first yield
>>> gen.send(10) # goes into 'x' variable
20
>>> next(gen) # run up to the next yield
>>> gen.send(6) # goes into 'x' again
12
>>> next(gen) # run up to the next yield
>>> gen.send(94.3) # goes into 'x' again
188.5999999999999
仅使用 yield
无法完成此操作。
至于为什么它有用,我见过的最好用例之一是 Twisted 的 @defer.inlineCallbacks
。基本上,它允许您编写这样的函数:
@defer.inlineCallbacks
def doStuff():
result = yield takesTwoSeconds()
nextResult = yield takesTenSeconds(result * 10)
defer.returnValue(nextResult / 10)
发生的情况是takesTwoSeconds()
返回一个Deferred
,这是一个承诺值,承诺稍后将计算出一个值。Twisted可以在另一个线程中运行计算。当计算完成时,它将其传递到deferred中,然后该值被发送回doStuff()
函数。因此,doStuff()
看起来可能与普通的过程函数类似,但它可以执行各种计算和回调等操作。在此功能之前的替代方法将是执行以下操作:
def doStuff():
returnDeferred = defer.Deferred()
def gotNextResult(nextResult):
returnDeferred.callback(nextResult / 10)
def gotResult(result):
takesTenSeconds(result * 10).addCallback(gotNextResult)
takesTwoSeconds().addCallback(gotResult)
return returnDeferred
它更为复杂和难以操作。
这个函数用于编写协程。
def coroutine():
for i in range(1, 10):
print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
while True:
print("From user {}".format(c.send(1)))
except StopIteration: pass
打印
From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...
看到控制权如何来回传递了吗?这些就是协程。它们可以用于各种很酷的事情,比如异步IO和类似的东西。
你可以这么想,在没有 send 的生成器中,它就像是一条单行道。
========== yield ========
Generator | ------------> | User |
========== ========
但是使用send,它就变成了双向通道。
========== yield ========
Generator | ------------> | User |
========== <------------ ========
send
这为用户在操作期间自定义生成器的行为打开了大门,并且生成器会响应用户。
send()
时,生成器还没有到达关键字 yield
,所以这样做是有一定道理的。 - Michael这可能会对某些人有所帮助。以下是一个生成器,不受send函数的影响。它在实例化时接收数字参数,并不受send的影响:
>>> def double_number(number):
... while True:
... number *=2
... yield number
...
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256
现在,以下是使用send函数执行相同类型的函数的方法,因此在每次迭代中,您可以更改number的值:
def double_number(number):
while True:
number *= 2
number = yield number
这是它的外观,如您所见,发送一个新值以供“number”使用会更改结果:
>>> def double_number(number):
... while True:
... number *= 2
... number = yield number
...
>>> c = double_number(4)
>>>
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6
你也可以将它放在一个 for 循环中,如下所示:
for x in range(10):
n = c.send(n)
print n
想要更多帮助,可以查看这个绝佳教程。
send
函数用途的一个说明性例子呢?一个简单的lambda x: x * 2
可以以更简洁的方式完成同样的事情。 - user209974a=b
中,首先评估右侧。a[p('left')] = p('right')
,先评估右侧。>>> def p(side):
... print(side)
... return 0
...
>>> a[p('left')] = p('right')
right
left
>>>
>>>
>>> [p('left'), p('right')]
left
right
[0, 0]
yield是什么?yield会暂停函数的执行并返回到调用者,并在恢复执行时继续执行之前离开的地方。
执行被暂停在哪里?你可能已经猜到了...执行在yield表达式的左右两侧之间被暂停。 所以new_val = yield old_val
,执行在=
号上停止,右边的值(在暂停之前,也是返回给调用者的值)可能与左边的值(在恢复执行后要分配的值)不同。
yield
产生2个值,一个在右侧,另一个在左侧。
如何控制yield表达式左侧的值?通过.send()
方法。
恢复后yield表达式的值取决于恢复执行的方法。如果使用
__next__()
(通常通过for或next()
内置函数)则结果为None。否则,如果使用send()
,则结果将是传递给该方法的值。
send()
的一些用例带有send()
的生成器允许:
以下是一些用例:
假设我们有一个期望按某个顺序预定义一组输入的食谱。
我们可以:
watched_attempt
实例每次输入时都检查输入是否为预期的(如果不是,则失败)
def recipe():
pot = []
action = yield pot
assert action == ("add", "water")
pot.append(action[1])
action = yield pot
assert action == ("add", "salt")
pot.append(action[1])
action = yield pot
assert action == ("boil", "water")
action = yield pot
assert action == ("add", "pasta")
pot.append(action[1])
action = yield pot
assert action == ("decant", "water")
pot.remove("water")
action = yield pot
assert action == ("serve")
pot = []
yield pot
要使用它,首先创建watched_attempt
实例:>>> watched_attempt = recipe()
>>> watched_attempt.next()
[]
调用 .next()
是启动生成器执行的必要步骤。
返回的值显示,我们的锅目前是空的。
现在按照食谱的要求执行一些操作:
>>> watched_attempt.send(("add", "water"))
['water']
>>> watched_attempt.send(("add", "salt"))
['water', 'salt']
>>> watched_attempt.send(("boil", "water"))
['water', 'salt']
>>> watched_attempt.send(("add", "pasta"))
['water', 'salt', 'pasta']
>>> watched_attempt.send(("decant", "water"))
['salt', 'pasta']
>>> watched_attempt.send(("serve"))
[]
正如我们所看到的,锅最终变空了。
如果有人不按照食谱去做,它就会失败(这可能是试图做饭时期望的结果 - 只是发现我们在给定指示时没有付足够的注意力而已学习过程)。
>>> watched_attempt = running.recipe()
>>> watched_attempt.next()
[]
>>> watched_attempt.send(("add", "water"))
['water']
>>> watched_attempt.send(("add", "pasta"))
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))
/home/javl/sandbox/stack/send/running.py in recipe()
29
30 action = yield pot
---> 31 assert action == ("add", "salt")
32 pot.append(action[1])
33
AssertionError:
请注意:
我们可以使用生成器来跟踪发送给它的值的累加总数。
每次添加数字时,都会返回输入计数和总和(对于前一个输入的时刻有效)。
from collections import namedtuple
RunningTotal = namedtuple("RunningTotal", ["n", "total"])
def runningtotals(n=0, total=0):
while True:
delta = yield RunningTotal(n, total)
if delta:
n += 1
total += delta
if __name__ == "__main__":
nums = [9, 8, None, 3, 4, 2, 1]
bookeeper = runningtotals()
print bookeeper.next()
for num in nums:
print num, bookeeper.send(num)
输出结果如下:
RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
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)
的简写方式 -- 在第一个 send
调用中传递除 None
之外的任何内容都是非法的)。它可能有一个结束:当生成器函数体中没有更多可达到的 yield
语句时。
你看到了什么使得 yield
语句(或更准确地说,生成器)如此特殊吗?与微不足道的 return
关键字不同,yield
能够在不终止其所在函数的情况下向其调用者传递值并从其调用者接收值!(当然,如果您确实希望终止函数或生成器,则也很方便使用 return
关键字。)遇到 yield
语句时,生成器函数暂停,然后在被发送另一个值时恢复,并在离开的地方继续执行。而 send
只是与生成器函数内部通信的接口。
如果我们真的想尽可能地分解这个推/拉/管道类比,我们最终会得到以下伪代码,它真正说明了除步骤 1-5 外,yield
和 send
是同一枚硬币管道的两个面:
right_end.push(None) # g.send的前半部分;发送None是启动生成器的方式
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("刚启动的生成器不能发送非None值")
left_end.do_stuff()
left_end.push(y) # yield的前半部分
left_end.pause()
right_end.resume()
value1 = right_end.pull() # g.send的后半部分
right_end.do_stuff()
right_end.push(value2) # 再次执行g.send的前半部分,但使用不同的值
right_end.pause()
left_end.resume()
x = left_end.pull() # yield的后半部分
跳至步骤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 # 丢弃值
。
有关通过管道发送值的确切顺序的详细信息,请参见下面的内容。
接下来是上述内容的一个相当长的具体模型。首先,应该注意到对于任何生成器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()
。right_end
定义了 send
函数,它是我们在前面用两行代码替换 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
的值首先被推入(yielded),然后所有被发送的值都被推回(yielded)。在第二个例子中,在首次到达push
时,x
没有值(尚未定义),因此会引发UnboundLocalError
错误。yield
的关键字呢? - Joshsend
之前,光标不会向前移动;需要调用一次 send(None)
将光标移动到第一个 yield
语句,然后才能通过后续的 send
调用实际发送“真实”值给 yield
。 - BallpointBenf
将会在某个时刻yield
,因此等待直到它从调用者那里收到一个send
?对于普通的函数调用,解释器会立即开始执行f
,对吗?毕竟,在 Python 中没有任何类型的AOT编译。你确定是这样吗?(我不怀疑你说的话,我只是对你在这里写的内容感到困惑)。我在哪里可以读到更多关于Python如何知道需要在开始执行函数的其余部分之前等待的信息? - Joshsend(None)
产生适当的值(例如 1
)而不需要将 None
发送到生成器中,这表明第一次调用 send
是一个特殊情况。这是一个棘手的接口设计;如果让第一个 send
发送任意值,那么产生的值和发送的值的顺序就会比当前的顺序少一个。 - BallpointBen这些也让我感到困惑。以下是我在设置一个交替发出和接收信号的生成器(yield、accept、yield、accept)时所做的示例...
def echo_sound():
thing_to_say = '<Sound of wind on cliffs>'
while True:
thing_to_say = (yield thing_to_say)
thing_to_say = '...'.join([thing_to_say]+[thing_to_say[-6:]]*2)
yield None # This is the return value of send.
gen = echo_sound()
print 'You are lost in the wilderness, calling for help.'
print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Hello!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)
print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Is anybody out there?'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)
print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Help!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)
You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
itr.send(None)
等同于next(itr)
,它所做的是提取生成器中yield关键字所返回的值。
下面有一个例子可以很清晰地展示这个概念,并且说明它如何在实际中使用。
def iterator_towards(dest=100):
value = 0
while True:
n = yield value
if n is not None:
dest = n
if dest > value:
value += 1
elif dest < value:
value -= 1
else:
return
num = iterator_towards()
for i in num:
print(i)
if i == 5:
num.send(0)
0
1
2
3
4
5
3
2
1
0
i == 5
的代码告诉它发送0
。这在iterator_towards中不是None
,因此它改变了dest
的值。然后我们向0
迭代。
但请注意,值5后面没有值4。这是因为.send(0)
的特性,它被yield了4
的值,但该值没有被打印。
如果我们加上continue
,我们就可以重新yield相同的值。
def iterator_towards(dest=100):
value = 0
while True:
n = yield value
if n is not None:
dest = n
continue
if dest > value:
value += 1
elif dest < value:
value -= 1
else:
return
这将允许您迭代一个列表,同时也可以动态地即时发送新的目标值。
send()
启动生成器时,必须使用None
作为参数进行调用,因为没有yield
表达式可以接收该值。” 这是从官方文档中引用的,并且问题中缺少这个引用。 - Rick