Python 生成器上的“发送”函数的用途是什么?

2025-01-08 08:50:00
admin
原创
112
摘要:问题描述:我明白yield。但是生成器的send功能是什么?文档说:generator.send(value) 恢复执行并将值“发送”到生成器函数中。value参数将成为当前表达式的结果yield。该send()方法返回生成器产生的下一个值,或者StopIteration如果生成器退出而未产生其他值,则引发。...

问题描述:

我明白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函数访问。作为管道各自两端的单数接口,yieldsend执行双重任务:它们各自将值推入/拉出管道两端,向右推,向左拉,而yield向右推,向左拉,则send相反。这种双重职责是围绕语句语义混淆的症结所在x = yield y。将yieldsend分解为两个明确的推/拉步骤将使它们的语义更加清晰:

  1. 假设g是生成器。g.send通过管道的右端向左推送一个值。

  2. 在暂停的上下文中执行g,允许生成器函数的主体运行。

  3. 推入的值g.send被向左拉出yield并被接收在管道的左端。在 中x = yield yx被分配给拉出的值。

  4. yield执行在生成器函数的主体内继续,直到到达下一行。

  5. yield将值通过管道左端向右推送,回到g.send。在 中x = yield yy通过管道向右推送。

  6. 生成器函数主体内的执行暂停,从而允许外部范围从其中断的地方继续执行。

  7. g.send恢复并提取值并将其返回给用户。

  8. 下次调用时g.send,返回步骤 1。

虽然是循环的,但这个过程确实有一个开始:当g.send(None)—— 缩写next(g)—— 首次调用时(将None除首次send调用之外的其他内容传递给其他调用是非法的)。并且它可能有一个结束:当生成器函数主体中没有更多yield语句可达到时。

你知道是什么让yield语句(或者更准确地说,生成器)如此特别吗?与微不足道的return关键字不同,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)each 拆分成两个语句:left_end.push(y)and x = left_end.pull(); and value1 = right_end.pull()and right_end.push(value2)。关键字有两种特殊情况yieldx = yieldand yield y。它们分别是 和x = 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. 我们已经定义了send函数 for ,它是上一步中right_end我们用来替换语句的两行对应的部分。x = yield y

在这个幻想世界中,函数在返回、g赋值right_endimpl()调用后可以继续执行。因此,在上面的例子中,如果我们逐行跟踪执行,将大致发生以下情况:

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。这不在Noneiterator_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
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1579  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1355  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   8  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   9  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用