混合产量和return。 `产量[cand]; return` 对比 `return [[cand]]`。为什么它们会导致不同的输出?

Mixing yield and return. `yield [cand]; return` vs `return [[cand]]`. Why do they lead to different output?

为什么

yield [cand]
return

导致 output/behavior 不同于

return [[cand]]

最小可行示例

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))) # []

最小可行反例

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]] 无效。

在生成器函数中,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 值在某种意义上确实得到了“returned”(它并没有完全被丢弃),但是它从来没有被任何正常迭代的东西看到,所以它在很大程度上是无用的.除了涉及将生成器用作协程的罕见情况(您在生成器的实例上使用 .send().throw() 并使用 next(genobj) 手动推进它),return 值不会看到发电机。

总之,你要选一个:

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

作为解释 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 循环的扩展形式 寻找 StopIteration 来指示循环结束,但它不使用它。对于任何不是生成器的东西, StopIteration 从来没有任何关联值; for 循环无法报告它们,即使它报告了(当它被告知迭代结束时它必须结束循环,并且 StopIteration 的参数显然不是迭代值的一部分) .消耗生成器的任何其他东西(例如调用 list )与 for 循环大致相同,以相同的方式忽略 StopIterationnothing except code that specifically expects generators (相对于更通用的迭代器和迭代器)将 ever 打扰检查 StopIteration 对象(在 C 层,大多数迭代器甚至不生成 StopIteration 对象的优化;它们 return NULL 并保留设置异常空,所有使用事物的迭代器协议都知道等同于 returning NULL 并设置一个 StopIteration 对象,因此对于除了生成器之外的任何东西,甚至没有异常检查当时)。