Python 3: 生成器的send方法

37

我不理解send方法。我知道它用于操作生成器,但是语法是这样的:generator.send(value)

我无法理解为什么值应该成为当前yield表达式的结果。我准备了一个例子:

def gen():
    for i in range(10):
        X = yield i
        if X == 'stop':
            break
        print("Inside the function " + str(X))

m = gen()
print("1 Outside the function " + str(next(m)) + '\n')
print("2 Outside the function " + str(next(m)) + '\n')
print("3 Outside the function " + str(next(m)) + '\n')
print("4 Outside the function " + str(next(m)) + '\n')
print('\n')
print("Outside the function " + str(m.send(None)) + '\n') # Start generator
print("Outside the function " + str(m.send(77)) + '\n')
print("Outside the function " + str(m.send(88)) + '\n')
#print("Outside the function " + str(m.send('stop')) + '\n')
print("Outside the function " + str(m.send(99)) + '\n')
print("Outside the function " + str(m.send(None)) + '\n')

结果是:

1 Outside the function 0

Inside the function None
2 Outside the function 1

Inside the function None
3 Outside the function 2

Inside the function None
4 Outside the function 3



Inside the function None
Outside the function 4

Inside the function 77
Outside the function 5

Inside the function 88
Outside the function 6

Inside the function 99
Outside the function 7

Inside the function None
Outside the function 8

说实话,这让我感到惊讶。

  1. 在文档中,我们可以读到当执行yield语句时,生成器的状态被冻结,并将expression_list的值返回给next的调用者。但是事实上好像并没有发生。为什么我们可以在gen()内部执行if语句和print函数呢?
  2. 如何理解函数内外的变量X不同?假设send(77)将77传递给m。那么yield表达式就变成了77。那么X = yield i是什么意思呢?77在函数内部是如何转换为在外部发生时的5的呢?
  3. 为什么第一个结果字符串没有反映出生成器内部发生的任何事情?

总之,您能否对这些sendyield语句进行一些注释呢?

5个回答

71
当您在生成器中使用send和表达式yield时,您将其视为协程;一种可以按顺序交错运行但不能与调用方并行的独立执行线程。
当调用方执行R = m.send(a)时,它将对象a放入生成器的输入槽中,将控制权转移到生成器,并等待响应。生成器将对象a作为X = yield i的结果接收,并运行直到遇到另一个yield表达式,例如Y = yield j。然后,它将j放入其输出槽中,将控制权传回给调用方,并等待再次恢复。调用方将j作为R = m.send(a)的结果接收,并运行直到遇到另一个S = m.send(b)语句,以此类推。

R = next(m) 就等同于 R = m.send(None);它是将 None 放入生成器的输入槽中,因此如果生成器检查 X = yield i 的结果,则 X 将为 None

作为一个比喻,考虑一个 dumb waiter

Dumb waiter

当服务器收到客户的订单时,他们将垫子放在餐车中,发送到厨房,并在出餐口等待菜品。
R = kitchen.send("Ham omelette, side salad")

这位厨师(一直在等待)拿起订单,准备菜品,将其提交给餐厅,并等待下一个订单:

next_order = yield [HamOmelette(), SideSalad()]

服务器(一直在舱口等待)将菜肴送到客户那里,然后返回并接受下一个订单,如此往复。由于服务员和厨师在发送订单或交付菜肴后都在舱口等待,因此任何时候只有一个人在工作,即该过程是单线程的。双方都可以使用正常的控制流程,因为生成器机制(也就是哑铃)会处理交错执行。

3
它是并发运行的,但不是并行运行。 - jfs
1
@J.F.Sebastian 我认为“concurrently”意味着并行执行? - ecatmur
7
在编程中,“parallel”意味着“concurrent”,但是通常的情况下并不成立相反的说法。并发编程和并行编程的区别 - jfs
2
这个隐喻太棒了。让我的困惑消失了。谢谢。 - joe wong
这个让我恍然大悟。 - Lucubrator
显示剩余2条评论

36

最令人困惑的部分应该是这一行X = yield i,特别是当您在生成器上调用send()时。实际上,您需要知道的唯一一件事是:

在词法层面上: next()等同于send(None)

在解释器层面上: X = yield i等同于以下行(顺序很重要):

yield i
# won't continue until next() or send() is called
# and this is also the entry point of next() or send()
X = the_input_of_send

而且,这两行注释正是我们需要在第一次调用send(None)的确切原因,因为生成器会在将值分配给X之前返回i(yield i


3
太好了!简洁易懂的解释,你非常准确地指出了弱点 X = yield i。我会给你点赞10次。 - Ciprian Tomoiagă
这真的很有帮助! - Tommy
1
由于我们只能在生成器执行到yield语句时才能为变量分配值,因此我们需要首先调用g.send(None)next(g)。实际上,在没有执行生成器到yield语句的情况下向生成器发送除None以外的值会导致TypeError - Han
@Han谢谢你的解释。这是不是意味着第一个 g.send(None) 会导致生成器执行到yield之前但不包括它,然后我可以使用 g.send(7) 来让 X 获取值7 - user2297550
1
@user2297550 不是的,第一次调用 g.send(None) 包括了第一个 yield 语句的执行。换句话说,在第一个 yield 语句执行之后才允许向生成器发送值。我理解发送值 g.send(v) 是替换了一个 yield 语句,例如,y = yield x 中的 rhs (yield x)。我认为以下问题的答案会帮助你理解。https://dev59.com/T2Ij5IYBdhLWcg3wx34x - Han
显示剩余3条评论

14

注意:
为简单起见,我的答案仅限于生成器在每行最多有1个yield命令的情况。

TL;DR:

  • .send()方法:

    • 发送一个值到当前挂起的yield命令(唤醒它),但是
    • 接收来自下一个即将到来的yield命令的值。
  • .send()方法发送的值的接收者是yield表达式本身。
    这意味着表达式yield 7

    • 产生值7,但是
    • 它自己的值,即(yield 7)的值,可以是例如"hello"
      (除了最简单的情况外,括号通常是必需的)- 如果这个yield 7命令被.send("hello")方法唤醒。

总体概述:

第一个带有None参数的send函数会启动生成器实例,从而开始执行其命令。

enter image description here


详细说明:

前言:

g = gen(),即 g 是生成器迭代器 gen() 的一个实例
(如下图片所示)。

  • 命令 next(g) 的行为与 g.send(None) 完全相同,因此您可以使用任何一种方式。

  • 只有在实例 g 在带有 yield 命令的语句处被挂起时,才允许发送一个非-None
    enter image description here

    • 为什么? 因为 .send() 方法只能将值发送到等待(挂起)的yield表达式(请参见下面“逐步”部分中的第4点)。

    因此,在发送非-None值之前,我们必须通过向其发送None值将生成器实例放置在这样的挂起状态中。 这可能就像 g.send(None) 那样简单:

    enter image description here

  • 但就在 g 成为挂起状态之前,它会产生 yield 命令的值。 这个产生的值成为 .send() 方法的返回值:

    enter image description here

    我们可能想要使用这个接收到的值或将其保存在变量中以供以后使用,因此与前两个图片不同,让我们从这个开始我们的旅程:


步骤:

  1. The first .send() starts the instance g. Instance g begins to execute its commands up to the first yield statement, which yields its value:

    enter image description here

    It means, that in the variable from_iterator_1 will be the string "first_from_iterator".

     

  2. Now, after yielding its first value, we have g in the suspended state

    enter image description here

    which allows us sending to g something useful, other than None — e.g. the number 1.

     

  3. So let's send the number 1 to g: enter image description here

     

  4. As g was suspended at the expression yield "first_from_iterator", the value of this expression (itself) will become 1.

    (Yes, the yield "first_from_iterator" is an expression, likewise a + b is.)

    Recall that at this moment the value "first_from_iterator" is long time ago already yielded.

     

  5. The instance g then wakes up, and — in turn — the g.send() now waits for a returned value. enter image description here

     

  6. The previously suspended, now woken statement will be executed.
    (Before the suspension, it was not executed, it only yielded a value.) enter image description here In our simple case (the woken statement is yield "first_from_iterator") there remains nothing to be performed, but what about

    • saving the received value (1) to a variable for the later use instead?

      received_1 = yield "first_from_iterator"      
      
    • or performing a more complicated computation with it instead?

      result = 3 * (yield "first_from_iterator") + 2           # result: 5
      

     

  7. All consequent statements in g will be performed, but only up to the next statement with the yield command in it.

    enter image description here

     

  8. That next statement (with the yield command in it) yields a value

    enter image description here

    which suspends g again, and wakes up the waiting .send() method (by providing it the awaited — yielded — return value).

     

  9. It allows performing next commands after it:

    enter image description here

     

  10. Now we are in the same situation as in the point 2. — just before performing the (next) .send() method — so the story will be repeated.

    Note:
    It will be repeated with the same issue as in the last point of the “Preface” section above — we probably don't want to throw out the yielded value, so instead of the commands

    g.send(1)                          # Not very appropriate
    

    is better to use something as

    from_iterator_2 = g.send(1)        # Saving the 2nd yielded value
    

    (and similarly for the next g.send(2) command).


7
def gen():
    i = 1
    while True:
        i += 1
        x = yield i
        print(x)

m = gen()
next(m)
next(m)
m.send(4)

结果

None
4

请查看上面更简化的代码。
我认为导致你困惑的是 'x = yield i' 语句,这个语句并不是说从 send() 方法接受的值被赋值给了 i 再把 i 赋值给 x。 相反,值 i 由 yield 语句返回到生成器,x 是由 send() 方法赋值的,一个语句同时做两件事。


0

既然你甚至要求包括注释,请考虑以下情况:

def lambda_maker():
    def generator():
        value = None
        while 1:
            value = yield value
            value= value[0][1]
    f = generator()
    next(f)  # skip the first None
    return f.send  # a handy lambda value: value[0][1]

现在以下两行代码是等价的:

a_list.sort(key=lambda a: a[0][1])
a_list.sort(key=lambda_maker())

顺便提一下,在当前的(2018-05-26,GDPR后第1天☺)CPython2和CPython3实现中,第二行比第一行运行更快,但这与每个函数调用的帧对象初始化开销有关。

这里发生了什么?lambda_maker调用f=generator()并获取一个生成器;调用初始的next(f)开始运行生成器并消耗初始的None值,并在yield行处暂停。然后它将绑定方法f.send返回给其调用者。从此时起,每次调用此绑定方法时,generator.value本地接收绑定方法的参数,重新计算value,然后循环回来yield当前value的值,并等待下一个.send获取另一个值。

生成器对象保留在内存中,循环中它所做的全部工作是:

  • yield 返回当前结果(最初为None)
  • receive 接收另一个值(即任何人使用的.send参数)
  • 基于接收到的值重新计算当前结果
  • 循环回去

有一个明确的 a_list 会有助于理解这个例子... - Setop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接