Why does
yield [cand]
return
lead to different output/behavior than
return [[cand]]
Minimal viable example
- uses recursion
- the output of the version using
yield [1]; returnis different than the output of the version usingreturn [[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))) # []
Min viable counter example
- does not use recurion
- the output of the version using
yield [1]; returnis the same as the output of the version usingreturn [[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]]
Full context
I'm solving Leetcode #39: Combination Sum and was wondering why one solution works, but not the other:
Working solution
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)
Non-working solution
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)
Output
On the following input
candidates = [2,3,6,7]
target = 7
print(Solution().combinationSum(candidates, target))
the working solution correctly prints
[[3,2,2],[7]]
while the non-working solution prints
[]
I'm wondering why yield [cand]; return works, but return [[cand]] doesn't.
In a generator function,
returnjust defines the value associated with theStopIterationexception implicitly raised to indicate an iterator is exhausted. It's not produced during iteration, and most iterating constructs (e.g.forloops) intentionally ignore theStopIterationexception (it means the loop is over, you don't care if someone attached random garbage to a message that just means "we're done").For example, try:
As you can see, your
returnvalue does get "returned" in a sense (it's not completely discarded), but it's never seen by anything iterating normally, so it's largely useless. Outside of rare cases involving using generators as coroutines (where you're using.send()and.throw()on instances of the generator and manually advancing it withnext(genobj)), the return value of a generator won't be seen.In short, you have to pick one:
yieldanywhere in a function, and it's a generator (whether or not the code path of a particular call ever reaches ayield) andreturnjust ends generation (while maybe hiding some data in theStopIterationexception). No matter what you do, calling the generator function "returns" a new generator object (which you can loop over until exhausted), it can never return a raw value computed inside the generator function (which doesn't even begin running until you loop over it at least once).yield, andreturnworks as expected (because it's not a generator function).As an example to explain what happens to the
returnvalue in normal looping constructs, this is whatfor x in gen():effectively expands to a C optimized version of:As you can see, the expanded form of the
forloop has to look for aStopIterationto indicate the loop is over, but it doesn't use it. And for anything that's not a generator, theStopIterationnever has any associated values; theforloop has no way to report them even if it did (it has to end the loop when it's told iteration is over, and the arguments toStopIterationare explicitly not part of the values iterated anyway). Anything else that consumes the generator (e.g. callingliston it) is doing roughly the same thing as theforloop, ignoring theStopIterationin the same way; nothing except code that specifically expects generators (as opposed to more generalized iterables and iterators) will ever bother to inspect theStopIterationobject (at the C layer, there are optimizations thatStopIterationobjects aren't even produced by most iterators; they returnNULLand leave the set exception empty, which all iterator protocol using things know is equivalent to returningNULLand setting aStopIterationobject, so for anything but a generator, there isn't even an exception to inspect much of the time).