python生成器“发送”功能的目的?

时间:2013-10-10 17:38:19

标签: python

有人能举例说明为什么存在与Python生成器功能相关的“发送”功能吗?我完全理解屈服函数。但是,发送功能让我很困惑。关于这种方法的文档很复杂:

generator.send(value)
  

恢复执行并将值“发送”到生成器函数中。 value参数成为当前yield表达式的结果。 send()方法返回生成器产生的下一个值,如果生成器退出而不产生另一个值,则引发StopIteration。

这是什么意思?我认为价值是功能的输入?短语“send()方法返回生成器产生的下一个值”似乎也是yield函数的确切目的; yield返回生成器产生的下一个值......

有人能给我一个利用发送器生成器的例子吗?

8 个答案:

答案 0 :(得分:107)

它用于将值发送到刚刚产生的生成器。这是一个人为的(无用的)解释性示例:

>>> 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

这更令人费解和笨拙。

答案 1 :(得分:76)

此函数用于编写协同程序

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
...

了解控件是如何来回传递的?这些是协程。它们可用于各种酷炫的东西,如asynch IO和类似的东西。

想象一下,有了发电机而没有发送,这是一条单行道

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

但是随着发送,它变成了双向的街道

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

这为用户定制生成器行为动态以及响应用户的生成器打开了大门。

答案 2 :(得分:30)

这可能对某人有所帮助。这是一个不受发送功能影响的生成器。它接受实例化时的数字参数,不受发送:

的影响
>>> 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执行相同类型的函数,因此在每次迭代时您都可以更改数字的值:

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

如需更多帮助,请查看此great tutorial

答案 3 :(得分:13)

send方法实现了coroutines

如果你还没有遇到过Coroutines,那么他们很难解决问题,因为它们改变了程序流的方式。您可以阅读good tutorial了解更多详情。

答案 4 :(得分:12)

使用生成器和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:

请注意:

  • 有预期步骤的线性序列
  • 步骤可能有所不同(有些正在删除,有些正在添加到底池中)
  • 我们设法通过函数/生成器完成所有这些 - 不需要使用复杂的类或类似的 strutures。

正在运行总计

我们可以使用生成器来跟踪发送给它的值的运行总数。

任何时候我们添加一个数字,输入的数量和总和将被返回(有效期为 之前的输入发送到它的那一刻)。

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)

答案 5 :(得分:9)

send()方法可控制yield表达式左侧的值。

要了解收益率有何不同以及它具有什么值,首先让我们快速刷新评估python代码的顺序。

Section 6.15 Evaluation order

  

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产生2个值,一个在右边,另一个在左边。

如何控制yield表达式左侧的值?通过.send()方法。

6.2.9. Yield expressions

  

恢复后的yield表达式的值取决于恢复执行的方法。如果使用__next__()(通常通过for或next()内置),则结果为None。否则,如果使用send(),则结果将是传递给该方法的值。

答案 6 :(得分:6)

“产量”一词有两个含义:生产某种东西(例如,生产玉米),以及停止让某人/其他东西继续(例如,汽车生产给行人)。两种定义都适用于Python的yield关键字;生成器函数之所以与众不同,是与常规函数不同的是,可以将值“返回”给调用者,而只是暂停而不终止生成器函数。

最容易将发电机想象为双向管道的一端,该管道的一端为“左”,另一端为“右”。该管道是在生成器本身与生成器函数的主体之间发送值的媒介。管道的每一端都有两个操作:push,它发送一个值并阻塞直到管道的另一端提取该值,并且什么都不返回;和pull,它将阻塞直到管道的另一端推送一个值,然后返回推送的值。在运行时,执行在管道任一侧的上下文之间来回跳动-每一侧运行,直到将值发送到另一侧,此时它停止,让另一侧运行,并等待一个值返回,这时另一边停止并继续。换句话说,管道的每一端都从接收值到发送值之间一直运行。

管道在功能上是对称的,但是-按照惯例,我在这个答案中定义-左端仅在生成器函数的主体内可用,并且可以通过yield关键字进行访问,而右端生成器,可以通过生成器的send函数进行访问。作为yieldsend到管道各自末端的唯一接口,它们起着双重作用:它们都向/从管道末端推/拉值,yield向右推和向左拉,而send则相反。这种双重职责是围绕x = yield y之类的语句语义造成混乱的症结所在。将yieldsend分解为两个显式的推/拉步骤将使它们的语义更加清晰:

  1. 假设g是生成器。 g.send通过管道的右端向左推动一个值。
  2. g上下文中的执行会暂停,从而允许生成器函数的主体运行。
  3. g.send推送的值由yield向左拉,并在管道的左端接收。在x = yield y中,x被分配给提取值。
  4. 执行继续在生成器函数的主体内,直到到达包含yield的下一行为止。
  5. yield向右推一个值,穿过管道的左端,回到g.send。在x = yield y中,y通过管道向右推。
  6. 生成器函数主体中的执行暂停,从而使外部范围可以从中断处继续执行。
  7. g.send恢复并提取该值并将其返回给用户。
  8. 下次调用g.send时,请返回步骤1。

虽然是周期性的,但此过程确实是有一个开始的:首先调用g.send(None)(即next(g)的缩写)时(传递{以外的其他内容是非法的) {1}}到第一个None调用)。而且它可能有一个结局:当生成器函数的主体中不再有send语句要到达时。

您看到yield语句(或更准确地说,生成器)如此特别的原因吗?与简单的yield关键字不同,return能够将值传递给其调用方并从其调用方接收值,而不会终止其所驻留的功能! (当然,如果您确实希望终止函数或生成器,也可以使用yield关键字很方便。)当遇到return语句时,生成器函数只会暂停,然后在发送另一个值后从上次中断的地方开始重新选择。 yield只是用于从外部与生成器函数内部进行通信的接口。

如果我们真的想尽可能地分解这种推/拉/管道类比,除以下步骤1-5,send和{ {1}}是同一 coin 管道的两侧:

  1. yield
  2. send
  3. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  4. right_end.pause()
  5. left_end.start()
  6. initial_value = left_end.pull()
  7. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  8. left_end.do_stuff()
  9. left_end.push(y) # the first half of yield
  10. left_end.pause()
  11. right_end.resume()
  12. value1 = right_end.pull() # the second half of g.send
  13. right_end.do_stuff()
  14. right_end.push(value2) # the first half of g.send (again, but with a different value)
  15. right_end.pause()
  16. left_end.resume()

关键的转换是我们将x = left_end.pull() # the second half of yieldgoto 6分别分成两个语句:x = yield yvalue1 = g.send(value2);以及left_end.push(y)x = left_end.pull()value1 = right_end.pull()关键字有两种特殊情况:right_end.push(value2)yield。这些分别是x = yieldyield 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的转换中发生了以下情况:

  1. 我们已将实现转移到嵌套函数中。
  2. 我们创建了一个双向管道,该管道的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是我们所知道的作为生成器对象。
  3. 在嵌套函数中,我们要做的第一件事是检查right_end是否为right_end,并在此过程中消耗推入值。
  4. 在嵌套函数中,语句left_end.pull()已替换为两行:Nonex = yield y
  5. 我们为left_end.push(y)定义了x = left_end.pull()函数,这与在上一步中用send语句替换的两行相对应。

在这个幻想的世界中,函数可以在返回后继续运行,right_end被分配了x = yield y,然后调用了g。因此,在上面的示例中,如果我们逐行执行,将发生以下大致情况:

right_end

这完全映射到上面的16个步骤的伪代码。

还有其他一些细节,例如错误的传播方式以及到达生成器末端(管道关闭)时会发生什么,但这应该清楚当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

在大多数情况下,它们与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

答案 7 :(得分:1)

这些也使我感到困惑。这是我在尝试设置一个生成和接受交替顺序信号的生成器时所做的一个例子(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!"