Python 生成器上的“发送”函数的用途是什么?
- 2025-01-08 08:50:00
- admin 原创
- 112
问题描述:
我明白yield
。但是生成器的send
功能是什么?文档说:
generator.send(value)
恢复执行并将值“发送”到生成器函数中。
value
参数将成为当前表达式的结果yield
。该send()
方法返回生成器产生的下一个值,或者StopIteration
如果生成器退出而未产生其他值,则引发。
这是什么意思?我以为value
是生成器函数的输入?短语“该send()
方法返回生成器产生的下一个值”似乎也是 的确切目的yield
,它还返回生成器产生的下一个值。
send
有没有一个利用发电机完成某些无法完成的事情的例子yield
?
解决方案 1:
它用于将值发送到刚刚产生的生成器中。这是一个人为的(无用的)解释性示例:
>>> 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 可以在另一个线程中运行计算。计算完成后,它会将其传递给延迟,然后将值发送回函数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
它变得更加复杂和难以处理。
解决方案 2:
这个函数是写协程的
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 等。
想象一下,有发电机,没有发送,这是一条单行道
========== yield ========
Generator | ------------> | User |
========== ========
但通过发送,它变成了一条双向的街道
========== yield ========
Generator | ------------> | User |
========== <------------ ========
send
这为用户动态定制生成器行为以及生成器对用户的响应打开了大门。
解决方案 3:
这可能对某些人有帮助。这是一个不受 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
看起来是这样的,你可以看到发送一个新数字会改变结果:
>>> 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
如需更多帮助,请查看这个精彩的教程。
解决方案 4:
该send()
方法控制 Yield 表达式左边的值。
为了理解yield的不同之处和它所持有的值,让我们首先快速回顾一下python代码的执行顺序。
6.15 评估顺序
Python 从左到右计算表达式的值。请注意,在计算赋值时,先计算右侧的值,然后再计算左侧的值。
因此,首先对表达式的a = 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
产生两个值,一个在右边,另一个在左边。
如何控制yield表达式左边的值?通过.send()
方法。
6.2.9. Yield 表达式
恢复后的yield表达式的值取决于恢复执行的方法。如果
__next__()
使用(通常通过for或next()
内置函数),则结果为None。否则,如果send()
使用,则结果将是传递给该方法的值。
解决方案 5:
使用生成器的一些用例和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)
解决方案 6:
该send
方法实现了协同程序。
如果你还没有遇到过协程,那么理解它们会有些困难,因为它们会改变程序的流程。你可以阅读一篇很好的教程来了解更多详细信息。
解决方案 7:
“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
。在 中x = yield y
,y
通过管道向右推送。生成器函数主体内的执行暂停,从而允许外部范围从其中断的地方继续执行。
g.send
恢复并提取值并将其返回给用户。下次调用时
g.send
,返回步骤 1。
虽然是循环的,但这个过程确实有一个开始:当g.send(None)
—— 缩写next(g)
—— 首次调用时(将None
除首次send
调用之外的其他内容传递给其他调用是非法的)。并且它可能有一个结束:当生成器函数主体中没有更多yield
语句可达到时。
你知道是什么让yield
语句(或者更准确地说,生成器)如此特别吗?与微不足道的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)
each 拆分成两个语句:left_end.push(y)
and x = left_end.pull()
; and value1 = right_end.pull()
and right_end.push(value2)
。关键字有两种特殊情况yield
:x = yield
and 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
函数 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
, 还没有值(因此UnboundLocalError
会引发)。
解决方案 8:
这些也让我很困惑。下面是我在尝试设置一个以交替顺序(产生、接受、产生、接受)产生和接受信号的生成器时所做的示例...
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!"
解决方案 9:
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
。这不在None
iterator_towards 中,因此它会更改 的值dest
。然后我们朝 进行迭代0
。
但是请注意,值 5 后面没有值 4。这是因为它的本质.send(0)
是产生了4
值并且没有被打印出来。
如果我们添加一个,continue
我们就可以重新获得相同的值。
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
这将允许您迭代列表,但也可以动态地动态地向其发送新的目标值。
解决方案 10:
下面是生成器的简单示例及其.send()
用法。它返回并同时设置计数器。or i
在未提供任何参数时使用。第一个调用必须是next()
或.send(None)
。
def gen():
"""Generator summing by 1 with the possibility of setting a counter"""
i = 0
while True:
i = (yield i) or i
i += 1
if __name__ == '__main__':
g = gen()
print(f"{next(g)=}")
print(f"{g.send(3)=}")
print(f"{next(g)=}")
print(f"{g.send(None)=}")
print(f"{next(g)=}")
输出:
next(g)=0
g.send(3)=4
next(g)=5
g.send(None)=6
next(g)=7
解决方案 11:
一个非常简单的例子,解释生成器中的 send() 流程:
def test_yield_send():
print("test_yield_send()")
# The magic of send() is below:
# 'yield' acts as a placeholder for receiving values from 'send()'
v = yield # 'v' receives the value sent to the generator via 'send()'
# The generator yields 'v' back to the caller of 'send()'
yield v # 'v' is returned as the result of 'send()'
def main():
print("Hello World!")
# Create a generator object
g = test_yield_send()
# Send a value to the generator
print(g.send(None)) # This prints 'None' because 'v' is set to 'None' now
# Send a value to the generator
print(g.send(2)) # This prints '2' because 'v' is set to '2' now
输出:
Hello World!
test_yield_send()
None
2