python生成器"发送"功能的目的?

Tom*_*mmy 129 python

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

generator.send(value)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Cla*_*diu 118

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

你不能这样做yield.

至于为什么它有用,我见过的最好的用例之一是Twisted的@defer.inlineCallbacks.基本上它允许你编写这样的函数:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)
Run Code Online (Sandbox Code Playgroud)

会发生什么是takesTwoSeconds()返回a 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
Run Code Online (Sandbox Code Playgroud)

它更复杂,更笨拙.

  • @汤米:你不会的。第一个例子只是为了解释它的作用。第二个示例用于一个实际有用的用例。 (3认同)
  • @Tommy:我想说,如果你真的想知道,请查看 [本演示文稿](http://www.dabeaz.com/coroutines/index.html) 并完成所有工作。简短的回答是不够的,因为那样你就会说“但我不能这样做吗?” 等等。 (3认同)
  • 您能解释一下这是什么目的吗?为什么不能用double_inputs(startingnumber)和yield重新创建它? (2认同)

Dan*_*zer 84

这个函数是编写协同程序

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
Run Code Online (Sandbox Code Playgroud)

版画

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...
Run Code Online (Sandbox Code Playgroud)

看看控制是如何来回传递的?这些是协程.它们可以用于各种很酷的东西,比如asynch IO和类似的东西.

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

==========       yield      ========
Generator |   ------------> | User |
==========                  ========
Run Code Online (Sandbox Code Playgroud)

但随着发送,它成为一条双向的街道

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send
Run Code Online (Sandbox Code Playgroud)

这开启了大门,用户自定义生成行为上飞和发电机响应用户.

  • @Tommy因为在运行时无法将参数更改为生成器.你给它参数,它运行,完成.使用send,你给它参数,它运行一点,你发送一个值,它做一些不同的,重复 (10认同)
  • 你能解释一下在所有事情之前发送无的目的吗? (5认同)
  • 但是生成器函数可以采用参数."发送"如何超越向发生器发送参数? (2认同)
  • @Tommy这将重新启动生成器,这将导致您重做大量工作 (2认同)
  • @ShubhamAggarwal完成"启动"发电机.这只是需要做的事情.从你第一次调用`send()时开始考虑它时,它就有了一些意义,但是生成器还没有达到关键字`yield`. (2认同)
  • `yield i` 包含在两层连续的括号中。如果删除一层,则会出现语法错误。为什么?`yield i` 周围的那些内括号是什么意思? (2认同)

rad*_*tek 35

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

现在,您将使用send执行相同类型的函数,因此在每次迭代时您都可以更改数字的值:

def double_number(number):
    while True:
        number *= 2
        number = yield number
Run Code Online (Sandbox Code Playgroud)

这就是看起来的样子,因为你可以看到为数字发送一个新值会改变结果:

>>> 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
Run Code Online (Sandbox Code Playgroud)

你也可以把它放在for循环中:

for x in range(10):
    n = c.send(n)
    print n
Run Code Online (Sandbox Code Playgroud)

有关更多帮助,请查看此精彩教程.

  • 一个不受send()影响的函数与一个函数之间的比较确实有帮助.谢谢! (8认同)
  • 这如何成为“发送”目的的说明性示例?一个简单的“lambda x: x * 2”以更简单的方式完成同样的事情。 (2认同)

Jan*_*sky 16

一些用例使用发电机和 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
    
    Run Code Online (Sandbox Code Playgroud)

要使用它,首先要创建watched_attempt实例:

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
Run Code Online (Sandbox Code Playgroud)

调用.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"))                                                                            
[] 
Run Code Online (Sandbox Code Playgroud)

正如我们所见,锅最终是空的.

如果一个人不遵循食谱,它就会失败(看到尝试烹饪的东西可能是期望的结果 - 只是学习我们在给出指示时没有给予足够的重视.

>>> 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:
Run Code Online (Sandbox Code Playgroud)

请注意:

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

运行总计

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

每当我们添加一个数字时,都会返回一个输入计数和总和(对于先前输入发送到它的时间有效).

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)
Run Code Online (Sandbox Code Playgroud)

输出看起来像:

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)
Run Code Online (Sandbox Code Playgroud)

  • 我运行你的示例,在 python 3 中似乎 Watched_attempt.next() 必须替换为 next(watched_attempt)。 (5认同)

Joc*_*zel 13

send方法实现了协同程序.

如果你还没有遇到过Coroutines,那么它们很难处理,因为它们会改变程序的流动方式.您可以阅读一个很好的教程了解更多细节.


use*_*857 13

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]
Run Code Online (Sandbox Code Playgroud)

yield的作用是什么?yield,暂停函数的执行并返回到调用方,并在暂停之前停止的地方恢复执行。

究竟在哪里暂停执行?您可能已经猜到了…… 执行被挂在yield表达式的右侧和左侧之间。因此new_val = yield old_val,执行将在=符号处停止,右侧的值(在挂起之前,并且也是返回给调用方的值)可能与左侧的值(在恢复后分配的值)有所不同执行)。

yield 产生2个值,一个在右边,另一个在左边。

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

6.2.9。收益表达

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

  • 您的解释帮助我理解了协程如何比上面的其他示例更好地工作!谢谢 :) (3认同)

Bal*_*Ben 9

“产量”这个词有两个含义:生产某物(例如,生产玉米),和停下来让其他人/事物继续(例如,汽车让行给行人)。这两个定义都适用于 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)它的缩写——第一次被调用(传递除None第一次send调用之外的其他东西是非法的)。它可能有一个结束:当yield生成器函数的主体中没有更多的语句要到达时。

你看到是什么让yield语句(或更准确地说,生成器)如此特别吗?与 measlyreturn关键字不同,yield它能够将值传递给其调用者并从其调用者接收值,而无需终止其所在的函数!(当然,如果您确实希望终止一个函数——或一个生成器——使用return关键字也很方便。)当yield遇到一个语句时,生成器函数只是暂停,然后从它离开的地方重新开始在发送另一个值时关闭。并且send只是从外部与生成器函数内部进行通信的接口。

如果我们真的要下来,只要我们可以打破这种推/拉/管类比,我们结束了下面的伪代码,真正开车回家的是,除了步骤1-5,yield并且send是相同的,双方硬币管:

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. goto 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。它们分别是 forx = yield None和 的语法糖_ = yield y # discarding value

有关通过管道发送值的精确顺序的具体细节,请参见下文。


下面是上述内容的一个相当长的具体模型。首先,首先应该注意的是,对于任何生成器gnext(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
Run Code Online (Sandbox Code Playgroud)

现在,f粗略地将 desugars定义为以下普通(非生成器)函数:

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()
Run Code Online (Sandbox Code Playgroud)

在这种转换中发生了以下情况f

  1. 我们已将实现移动到嵌套函数中。
  2. 我们已经创建了一个双向管道,它left_end可以被嵌套函数right_end访问,并且可以被外部作用域返回和访问——right_end这就是我们所知道的生成器对象。
  3. 在嵌套函数中,我们做的第一件事就是检查left_end.pull()None,在消费过程中推值。
  4. 在嵌套函数中,该语句x = yield y已被替换为两行:left_end.push(y)x = left_end.pull()
  5. 我们已经定义了sendfor 函数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
Run Code Online (Sandbox Code Playgroud)

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

还有一些其他细节,比如错误是如何传播的,以及当你到达生成器的末端(管道关闭)时会发生什么,但这应该清楚地说明基本控制流在send使用时是如何工作的。

使用这些相同的脱糖规则,让我们看两个特殊情况:

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x
Run Code Online (Sandbox Code Playgroud)

在大多数情况下,它们的脱糖方式与 相同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
Run Code Online (Sandbox Code Playgroud)

在第一个中,传递给的值f1最初被推送(产生),然后所有被拉动(发送)的值被立即推送(产生)。在第二个中,x当它第一次出现时没有价值(还)push,所以 anUnboundLocalError被提高。


Pet*_*ter 5

这些也让我很困惑。这是我在尝试设置一个生成器时制作的一个示例,该生成器以交替顺序生成和接受信号(生成,接受,生成,接受)...

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)
Run Code Online (Sandbox Code Playgroud)

输出是:

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!"
Run Code Online (Sandbox Code Playgroud)


Tat*_*ize 5

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)
Run Code Online (Sandbox Code Playgroud)

这将打印:

0
1
2
3
4
5
3
2
1
0
Run Code Online (Sandbox Code Playgroud)

处的代码i == 5告诉它发送0。这不在Noneiterator_towards 中,因此它改变了 的值dest。然后我们迭代到0

但请注意,值 5 之后没有值 4。这是因为 的本质.send(0)是生成该4值并且未打印该值。

如果我们添加 acontinue我们可以重新产生相同的值。

0
1
2
3
4
5
3
2
1
0
Run Code Online (Sandbox Code Playgroud)

这将允许您迭代列表,但也可以动态地动态向其发送新的目标值。