Bumped into a behaviour I'm struggling to grasp without an assistance. Here's a recursive function:
OPERATORS = ['+', '-', '*', '/']
def recursive(value, operands):
if not operands:
return value
for operator in OPERATORS:
new_value = str(eval(value + operator + operands[-1]))
new_operands = operands[:-1]
yield from recursive(new_value, new_operands)
def countdown(value, operands):
return next(i for i in recursive(value, operands) if i == '0')
ans = countdown('5', ['1', '2', '3'])
print(ans)
return value
raises StopIteration
which is not handled by caller so exception is raised before anything returns.
If return value
is substituted by yield value; return
like this:
def recursive(value, operands):
if not operands:
yield value
return
for operator in OPERATORS:
new_value = str(eval(value + operator + operands[-1]))
new_operands = operands[:-1]
yield from recursive(new_value, new_operands)
or yield value; raise StopIteration
or yield value; raise StopIteration(value)
or loop is hidden under else
clause then exception is handled by caller the way I expect and function eventually returns '0'
. All of these also raise exception: raise StopIteration
, both bare and with argument, yield; return value
, and bare return
.
In short, caller breaks when StopIteration
is raised and yield
never returned.
Why?
PEP380 states that first StopIteration
is handled in a different way then others. PEP479 says:
Currently, StopIteration raised accidentally inside a generator function will be interpreted as the end of the iteration by the loop construct driving the generator.
Apparently, except the first time. Still, the details of underlying implementation and exact reasoning behind it are unclear to me.
Two more additional questions:
what's the right way of writing this snippet? return (yield value)?
is it a quirk, a feature, or something else?
Edit: fixed mistakes in code
Edit2: As far as I understand the execution flow in the first snippet is following:
countdown
creates generator fromrecursive('5', ['1', '2', '3'])
- Which spawns generators all the way down the tree to
recursive('11', [])
- At this point
StopIteration('11')
is raised
And here is the tricky part. What happens here? How StopIteration
is handled?
Second snippet:
- same
- same
'11'
is yielded upwards until it reaches countdown- Where it gets rejected by
if '11' == '0'
- Control flow reaches back to
yield '11'
and raisesStopIteration
- Repeat until
'0'
From what I see now that is pretty much expected behaviour. StopIteration
is intrecepted by it's caller and does not propagate upward. Caller in turn raises StopIteration
without arguments. That is why the '11'
part of an original exception never reached countdown
. Exception in the first snippet traceback was bare StopIteration
raised in countdown
by recursive('5', ['1', '2', '3']
.