211

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

generator.send(value)

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

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

有人可以给我一个使用 send 的生成器的示例,它可以完成 yield 无法完成的事情吗?

4

9 回答 9

203

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

>>> 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()返回 a Deferred,这是一个承诺稍后将计算一个值的值。Twisted 可以在另一个线程中运行计算。计算完成后,它会将其传递给 deferred,然后将值发送回doStuff()函数。因此,doStuff()can 最终看起来或多或少像一个普通的程序函数,除了它可以进行各种计算和回调等。在此功能之前的替代方法是执行以下操作:

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

它更加复杂和笨拙。

于 2013-10-10T17:47:01.287 回答
126

这个函数是写协程的

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

这为用户在运行中自定义生成器行为和生成器响应用户打开了大门。

于 2013-10-10T17:47:32.647 回答
73

这可能会帮助某人。这是一个不受发送函数影响的生成器。它在实例化时接受 number 参数并且不受 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

如需更多帮助,请查看这个很棒的教程

于 2014-10-06T13:48:50.753 回答
32

send()方法控制 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产生 2 个值,一个在右边,另一个在左边。

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

6.2.9。屈服表达式

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

于 2018-08-15T22:46:03.087 回答
22

使用生成器的一些用例和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()                                                                                     
[]                                                                                                     

调用 to.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)
于 2016-04-26T14:40:53.230 回答
16

send方法实现协程

如果您还没有遇到过协程,那么您很难理解它们,因为它们会改变程序的流动方式。您可以阅读一个很好的教程以获取更多详细信息。

于 2013-10-10T17:45:11.503 回答
10

“yield”这个词有两个含义:生产某物(例如,生产玉米),以及停下来让其他人/事物继续(例如,汽车让行人)。两个定义都适用于 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. In x = yield y,y被向右推通过管道。
  6. 生成器函数体内的执行暂停,允许外部作用域从它停止的地方继续。
  7. g.send恢复并拉取值并将其返回给用户。
  8. 下次调用时g.send,返回步骤 1。

虽然是循环的,但这个过程确实有一个开始:当g.send(None)-- 这是什么的缩写 -- 第一次被调用(除了第一次调用next(g)之外传递其他东西是非法的)。它可能有一个结束:当生成器函数的主体中没有更多的语句可以到达时。Nonesendyield

你看到是什么让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 yand value1 = g.send(value2)each 拆分为两个语句:left_end.push(y)and 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

现在,对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

  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

这完全映射到上面的 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,所以 anUnboundLocalError被提出。

于 2020-02-03T15:21:43.757 回答
5

这些也让我很困惑。这是我在尝试设置生成器时制作的示例,该生成器以交替顺序(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!"
于 2017-10-23T10:39:40.390 回答
1

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。这不在Noneiterator_towards 中,所以它改变了dest. 然后我们迭代到0

但是请注意,在值 5 之后没有值 4。这是因为它的性质.send(0)是它产生了4值并且没有打印出来。

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

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

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

于 2021-08-24T21:18:54.753 回答