2

为什么

yield [cand]
return

导致不同的输出/行为

return [[cand]]

最小可行示例

  • 使用递归
  • 使用的版本的输出与使用的版本的yield [1]; return输出不同return [[1]]
def foo(i):
    if i != 1:
        yield [1]
        return
    yield from foo(i-1)    

def bar(i):
    if i != 1:  
        return [[1]]
    yield from bar(i-1)

print(list(foo(1))) # [[1]]
print(list(bar(1))) # []

最小可行反例

  • 不使用递归
  • 使用的版本的输出与使用yield [1]; return的版本的输出相同return [[1]]
def foo():
    yield [1]
    return

def foofoo():
    yield from foo()

def bar():
    return [[1]]

def barbar():
    yield from bar()

print(list(foofoo())) # [[1]]
print(list(barbar())) # [[1]]

完整的上下文

我正在解决Leetcode #39: Combination Sum并且想知道为什么一种解决方案有效,而另一种则无效:

工作解决方案

from functools import cache # requires Python 3.9+

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        @cache
        def helper(targ, i=0):
            if i == N or targ < (cand := candidates[i]):
                return
            if targ == cand:
                yield [cand]
                return
            for comb in helper(targ - cand, i):
                yield comb + [cand]
            yield from helper(targ, i+1)
        
        N = len(candidates)
        candidates.sort()
        yield from helper(target)

非工作解决方案

from functools import cache # requires Python 3.9+

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        @cache
        def helper(targ, i=0):
            if i == N or targ < (cand := candidates[i]):
                return
            if targ == cand:
                return [[cand]]
            for comb in helper(targ - cand, i):
                yield comb + [cand]
            yield from helper(targ, i+1)
        
        N = len(candidates)
        candidates.sort()
        yield from helper(target)

输出

在以下输入

candidates = [2,3,6,7]
target = 7
print(Solution().combinationSum(candidates, target))

工作解决方案正确打印

[[3,2,2],[7]]

而非工作解决方案打印

[]

我想知道为什么yield [cand]; return有效,但return [[cand]]没有。

4

1 回答 1

1

在生成器函数中,return只需定义与隐式引发的异常关联的值,StopIteration以指示迭代器已用尽。它不是在迭代期间产生的,并且大多数迭代构造(例如for循环)故意忽略StopIteration异常(这意味着循环结束,您不在乎是否有人将随机垃圾附加到仅意味着“我们完成了”的消息)。

例如,尝试:

>>> def foo():
...     yield 'onlyvalue'  # Existence of yield keyword makes this a generator
...     return 'returnvalue'
...

>>> f = foo()  # Makes a generator object, stores it in f

>>> next(f)  # Pull one value from generator
'onlyvalue'

>>> next(f)  # There is no other yielded value, so this hits the return; iteration over
--------------------------------------------------------------------------
StopIteration                            Traceback (most recent call last)
...
StopIteration: 'returnvalue'

正如你所看到的,你的return值在某种意义上确实得到了“返回”(它没有被完全丢弃),但它从来没有被任何正常迭代的东西看到,所以它在很大程度上是无用的。除了涉及将生成器用作协程的罕见情况(您正在使用.send().throw()在生成器的实例上并使用 手动推进它next(genobj))之外,将不会看到生成器的返回值。

简而言之,您必须选择一个:

  1. 在函数中的yield 任何地方使用,它是一个生成器(无论特定调用的代码路径是否到达 a yield)并且只是结束生成(同时可能在异常return中隐藏一些数据)。StopIteration不管你做什么,调用生成器函数“返回”一个新的生成器对象(你可以循环直到用尽),它永远不会返回在生成器函数内部计算的原始值(它甚至不会开始运行,直到你循环至少一次)。
  2. 不要使用yield, 并按return预期工作(因为它不是生成器函数)。

作为解释return正常循环结构中的值发生了什么的示例,这for x in gen():有效地扩展为 C 优化版本:

__unnamed_iterator = iter(gen())
while True:
    try:
        x = next(__unnamed_iterator)
    except StopIteration:  # StopIteration caught here without inspecting it
        break              # Loop ends, StopIteration exception cleaned even from sys.exc_info() to avoid possible reference cycles

    # body of loop goes here

# Outside of loop, there is no StopIteration object left

如您所见,for循环的扩展形式必须寻找 aStopIteration来指示循环结束,但它不使用它。对于任何不是生成器的东西,它StopIteration永远不会有任何关联的值;即使这样做了,for循环也无法报告它们(当它被告知迭代结束时,它必须结束循环,并且参数 toStopIteration显然不是迭代值的一部分)。任何其他消耗生成器的东西(例如调用它)都在做与循环list大致相同的事情,以同样的方式忽略它;除了特别期望生成器(而不是更通用的迭代器和迭代器)的代码之外,什么都不会forStopIteration费心检查StopIteration对象(在 C 层,有一些优化,StopIteration大多数迭代器甚至都不会生成对象;它们返回NULL并将设置的异常留空,所有使用事物的迭代器协议都知道这相当于返回NULL和设置一个StopIteration对象,所以除了生成器之外的任何东西,大部分时间都没有例外检查)。

于 2022-02-17T20:38:38.923 回答