Python生成器中的“send”函数的目的是什么?

256

我了解yield。但是生成器的send函数是做什么的呢?文档中写道:

generator.send(value)
恢复执行并向生成器函数“发送”一个值。`value` 参数成为当前 `yield` 表达式的结果。`send()` 方法返回生成器产出的下一个值,如果生成器未产出其他值则引发 `StopIteration` 异常。请问是否有使用 `send` 的生成器示例,可以实现 `yield` 无法完成的功能?

6
duplicate:https://dev59.com/questions/GGcs5IYBdhLWcg3wtmMD - Bas Swinckels
3
当将回调函数转换为从内部使用的生成器时,添加了另一个现实生活示例(从FTP读取)。 - Jan Vlcinsky
4
值得一提的是,“当调用send()启动生成器时,必须使用None作为参数进行调用,因为没有yield表达式可以接收该值。” 这是从官方文档中引用的,并且问题中缺少这个引用。 - Rick
9个回答

245

它用于将值发送到刚刚yielded的生成器中。以下是一个人造(不实用)的解释性示例:

>>> 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可以在另一个线程中运行计算。当计算完成时,它将其传递到deferred中,然后该值被发送回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

它更为复杂和难以操作。


3
你能解释一下这个的目的是什么吗?为什么不能用double_inputs(startingnumber)和yield来重新创建它呢? - Tommy
为什么你会使用这个,而不是一个简单的函数来将输入值加倍呢? - Tommy
6
@Tommy: 你不会这样做。第一个例子只是为了解释它的作用。第二个例子则是一个真正有用的用例。 - Claudiu
3
@Tommy:我觉得如果你真的想知道,就查看这个演示文稿并仔细阅读它。简短的回答是不够的,因为你可能会问:“但我不能只是像这样做吗?”等等。 - Claudiu
1
非常好的解决方案,只是需要注意 next() 在 Python 3.x 中已经改变了。现在请使用 gen.next()。 - Hades
显示剩余5条评论

147

这个函数用于编写协程。

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和类似的东西。

你可以这么想,在没有 send 的生成器中,它就像是一条单行道。

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

但是使用send,它就变成了双向通道。

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

这为用户在操作期间自定义生成器的行为打开了大门,并且生成器会响应用户。


6
生成器函数可以带参数,但“Send”方法如何超越向生成器发送参数的功能? - Tommy
23
由于生成器在运行过程中无法更改参数,因此您无法更改生成器的参数。您给它参数,它运行,结束。使用send,您给它参数,它运行一段时间,然后您发送一个值,它会执行不同的操作,重复进行。 - daniel gratzer
3
@Tommy 这将重新启动发电机,意味着您需要重新做大量的工作。 - daniel gratzer
11
请问您能否解释一下在所有内容之前发送None的目的是什么? - Shubham Aggarwal
5
它是用来“启动”生成器的。这只是需要完成的一个步骤。如果你思考一下,当你第一次调用 send() 时,生成器还没有到达关键字 yield,所以这样做是有一定道理的。 - Michael
显示剩余8条评论

101

这可能会对某些人有所帮助。以下是一个生成器,不受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

这是它的外观,如您所见,发送一个新值以供“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

想要更多帮助,可以查看这个绝佳教程


28
这个比较函数中,一个不受 send() 影响,而另一个却受到影响的例子,真的很有帮助。谢谢! - Manas Bajaj
2
这怎么能成为send函数用途的一个说明性例子呢?一个简单的lambda x: x * 2可以以更简洁的方式完成同样的事情。 - user209974
1
它使用send吗?去添加你的答案。 - radtek

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

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

6.2.9. Yield expressions

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


3
你的解释比上面其他例子好多了,帮助我更好地理解了协程的工作方式!!谢谢 :) - rtindru
“send()` 控制 yield 表达式 的结果,这样说不是更准确吗?” - bool3max
哪个结果?返回给调用者的还是函数内部分配给变量的结果。虽然这是正确的,但仍然不够清晰。 - user2059857

26

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

5
我运行了你的示例,在Python 3中似乎需要将watched_attempt.next()替换为next(watched_attempt)。 - thanos.a

17
< p > send 方法实现 协程

< p >如果您还没有遇到过协程,那么理解它们会有些棘手,因为它们改变了程序执行的方式。您可以阅读好的教程获取更多细节。


Dave Beazley是一位迷人的教师。 - Peter Moore

11
“yield”这个单词有两个意思:产生某物(例如产出玉米)和停下来让别人/其他事情继续(例如汽车为行人让路)。这两个定义都适用于Python的“yield”关键字;生成器函数的特殊之处在于,与常规函数不同的是,在暂停而不终止生成器函数的情况下,值可以“返回”给调用者。
最容易想象生成器是一个双向管道的一端,具有“左”端和“右”端;这个管道是在生成器本身和生成器函数体之间发送值的媒介。管道的每一端都有两个操作:推送(push),它发送值并阻塞,直到管道的另一端拉取该值,并且不返回任何内容;以及拉取(pull),它阻塞,直到管道的另一端推送值,并返回推送的值。在运行时,执行在管道的两端上下反弹——每一端运行,直到它向另一端发送一个值,此时它停止,让另一端运行,并等待返回一个值,此时另一端停止,它恢复。换句话说,管道的每一端从接收值的时刻开始运行,直到发送值的时刻结束。
管道在功能上是对称的,但根据我在这个答案中定义的约定,左端仅在生成器函数体内可用,并通过“yield”关键字访问,而右端是生成器本身,并通过生成器的“send”函数访问。作为它们各自管道端的单一接口,“yield”和“send”承担了双重职责:它们都将值推送到/从管道的两端,其中“yield”向右推送并向左拉取,而“send”执行相反操作。这种双重职责是导致类似于“x = yield y”语句语义混乱的关键。将“yield”和“send”分解为两个显式的推/拉步骤将使它们的语义更加清晰。
  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) 的简写方式 -- 在第一个 send 调用中传递除 None 之外的任何内容都是非法的)。它可能有一个结束:当生成器函数体中没有更多可达到的 yield 语句时。

你看到了什么使得 yield 语句(或更准确地说,生成器)如此特殊吗?与微不足道的 return 关键字不同,yield 能够在不终止其所在函数的情况下向其调用者传递值并从其调用者接收值!(当然,如果您确实希望终止函数或生成器,则也很方便使用 return 关键字。)遇到 yield 语句时,生成器函数暂停,然后在被发送另一个值时恢复,并在离开的地方继续执行。而 send 只是与生成器函数内部通信的接口。

如果我们真的想尽可能地分解这个推/拉/管道类比,我们最终会得到以下伪代码,它真正说明了除步骤 1-5 外,yieldsend 是同一枚硬币管道的两个面:

  1. right_end.push(None) # g.send的前半部分;发送None是启动生成器的方式
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("刚启动的生成器不能发送非None值")
  6. left_end.do_stuff()
  7. left_end.push(y) # yield的前半部分
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # g.send的后半部分
  11. right_end.do_stuff()
  12. right_end.push(value2) # 再次执行g.send的前半部分,但使用不同的值
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # yield的后半部分
  16. 跳至步骤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 # 丢弃值

有关通过管道发送值的确切顺序的详细信息,请参见下面的内容。


接下来是上述内容的一个相当长的具体模型。首先,应该注意到对于任何生成器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. 我们为 right_end 定义了 send 函数,它是我们在前面用两行代码替换 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的值首先被推入(yielded),然后所有被发送的值都被推回(yielded)。在第二个例子中,在首次到达push时,x没有值(尚未定义),因此会引发UnboundLocalError错误。

“g = f(1)” 中的参数1已经被正常捕获并分配给了f函数体内的y,但是while True循环还没有开始。为什么?为什么Python不会尝试运行这段代码,直到遇到例如yield的关键字呢? - Josh
@Josh 在第一次调用 send 之前,光标不会向前移动;需要调用一次 send(None) 将光标移动到第一个 yield 语句,然后才能通过后续的 send 调用实际发送“真实”值给 yield - BallpointBen
谢谢 - 这很有趣,所以解释器知道函数f 将会在某个时刻yield,因此等待直到它从调用者那里收到一个send?对于普通的函数调用,解释器会立即开始执行f,对吗?毕竟,在 Python 中没有任何类型的AOT编译。你确定是这样吗?(我不怀疑你说的话,我只是对你在这里写的内容感到困惑)。我在哪里可以读到更多关于Python如何知道需要在开始执行函数的其余部分之前等待的信息? - Josh
@Josh 我是通过观察不同的玩具生成器工作方式来构建这个心理模型的,没有任何对Python内部机制的理解。然而,初始的 send(None) 产生适当的值(例如 1)而不需要None 发送到生成器中,这表明第一次调用 send 是一个特殊情况。这是一个棘手的接口设计;如果让第一个 send 发送任意值,那么产生的值和发送的值的顺序就会比当前的顺序少一个。 - BallpointBen
谢谢BallpointBen。非常有趣,我留了一个问题在这里,想知道为什么会这样。 - Josh

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

4

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。这在iterator_towards中不是None,因此它改变了dest的值。然后我们向0迭代。

但请注意,值5后面没有值4。这是因为.send(0)的特性,它被yield了4的值,但该值没有被打印。

如果我们加上continue,我们就可以重新yield相同的值。

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

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


1
缺少第一行的一个“0”,关于它将打印什么的解释。 - Sean Mackesey
请注意,"在值5之后没有值4"是错误的 - 值4将被打印。在第二个示例中,值5将被打印两次。因此,这两个示例在解释上都是错误的。 - ustulation
1
0 1 2 3 4 5 4 3 2 1 0 - Tatarize

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