for / while else doesn't make sense

Steven D'Aprano steve at pearwood.info
Mon Jun 13 22:43:35 EDT 2016


On Tue, 14 Jun 2016 09:45 am, Michael Selik wrote:

> On Sun, Jun 12, 2016 at 10:16 PM Steven D'Aprano <steve at pearwood.info>
> wrote:
> 
>> On Mon, 13 Jun 2016 04:44 am, Michael Selik wrote:
>>
>> > On Sun, Jun 12, 2016 at 6:11 AM Steven D'Aprano <
>> > steve+comp.lang.python at pearwood.info> wrote:
>> >
>> >> - run the for block
>> >> - THEN unconditionally run the "else" block
>> >>
>> >
>> > Saying "unconditionally" is a bit misleading here. As you say, it's
>> > conditioned on completing the loop without break/return/raise.
>>
>> It's also conditional on the OS not killing the Python process,
>> conditional on the CPU not catching fire, conditional on the user not
>> turning the power of, and conditional on the sun not exploding and
>> disintegrating the entire earth.
>>
>> In the absence of any event which interferes with the normal execution of
>> code by the Python VM, and in the absence of one of a very few
>> explicit "JUMP" statements which explicitly jump out of the compound
>> for...else statement, the else clause is unconditionally executed after
>> the for clause.
>>
>> Happy now?
>>
> 
> I think most folks assume that their program will not run as expected if
> the sun explodes.

On this list, I daresay somebody will insist that if their computer is on
one of Jupiter's moons it will keep running fine and therefore I'm wrong.


> Saying that ``raise``, ``break``, and ``return`` are "one of a very few
> explicit JUMP statements" implies that they are obscure. 

What? No. How do you get that?

If I tell you that Python has only two loop constructs, for and while, would
that imply that they are rare and unusual? (Three if you count
comprehensions as distinct from the for statement.)

Python has only two conditional branches: if...elif..else, and the ternary
if operator. Does that make them obscure?

raise, break and return are all explicit JUMPs: they transfer execution to
some place other than the next executable line. There are others, including
continue, but they don't transfer execution past the for loop, so don't
matter in this context. None of this implies that they are obscure. I'm
sorry if you've never thought of a return or break as a JUMP before, but
that's what they are.


> Listing them in 
> addition to the sun exploding suggests that you think they are similarly
> unlikely and should be ignored as too bizarre to consider.

No. The sun exploding was me gently mocking you for your comment disputing
the "unconditional" part. Yes, you are technically right that technically
the "else" block will only run if no "break" is reached, and no "return" is
reached, no exception is raised, also that os._exit or os.abort aren't
called, the CPU doesn't catch fire, and the world isn't destroyed.

If we try to enumerate all the things which could prevent the "else" block
from running, we'll be here for decades. But, and this is the point that
everyone seems to have missed, * every single one of those things* is
completely independent of the for...else statement.

*Including* the presence or absence of a "break".

If you want to understand how Python statements work, you should understand
them in isolation (as much as possible), which then allows you to
extrapolate their behaviour in combination with other statements. Most
lines of Python code are understandable in isolation, or at least close to
isolation. You can get *very close* to a full understanding of Python by
just reading one line at a time (with perhaps a bit of short term memory to
remember if you are compiling a function, building a class, etc).

E.g. you don't need to understand for loops to understand if...else.

And vice versa: for...else has a well-defined meaning and operates in a
simple fashion in isolation of other language features. Its not compulsory
to put a "return" statement inside your for loop. Nor is it compulsory to
put a "raise" inside it. And "break" is not compulsory either.

If you think of for...else as being (in some sense) glued to break, then
what are you going to make of code with for...else and no break? That's
legal Python code, and somebody will write it, even if only by accident.

If you think of for...else as being glued to an if inside the for block,
then what are you going to make of code where the if already has an else?
Or code that unconditionally breaks inside the loop? Again, that's legal
code, even if useless. If you think that the for...else has to match an if
inside the loop, you'll have to invent special rules for when there is no
if, or ten of them, or they all are already matched with their own elses.

If you start thinking of it as code which is run conditionally "only if no
break was executed", that leads to people thinking of it in terms of some
sort of hidden flag that Python keeps to tell whether or not a break was
seen. A month or two ago, we had somebody, mislead by that mental model,
asking whether Python should expose that flag so he could write code
something like:

for x in seq:
   do_stuff()
else:
   do_something_else()
if MAGIC_FLAG:
   print("break")


(I don't quite remember all the details of the poster's question/proposal,
but the details aren't important. What's important is that he had a
misleading mental model of Python's for...else semantics that lead him to
make *incorrect predictions* of what Python can, or will, do.)

Somebody else suggested that "else" had the same semantics as "finally", in
that it was always executed after the for loop except for the one special
case of "break". And that mental model is wrong too. And another poster
carefully unrolled the loop to show that the else matched up with the if
inside the loop, and completely failed to deal with the case where the if
already has a matching else. Or the case where there's no break.

There's a simple mental model, one which has the advantage of actually
matching the implementation: for...else executes else unconditionally,
unless something, *anything*, (break, return, raise, the end of the world)
prevents it. As far as I can see, that model works under all possible
circumstances, now and in the future: one if or none, or even ten, with or
without their own matching elses, break or no break, return, raise, even
the end of the world. *wink*

If Python 3.6 introduces a GOTO command, then my mental model of for...else
doesn't need to change. (Neither will the implementation.) If Python 3.7
decides that functions can only have one exit, out the bottom of the
function, and removes the "return" statement, then my mental model of
for...else doesn't need to change either.

All mental models are imperfect, but I think mine is the least imperfect of
all those I've seen. I've struggled with understanding for...else for a
very long time, and I've tried out various models over the years. Starting
with the one implied by the name:


for x in sequence:
    block
else:
    # for...else is like if...else
    # one *or* the other runs but not both
    ...


That mental model was ludicrously wrong, but I suffered under it for *years*
and couldn't see why my for...else statements weren't doing what I wanted.

I then moved to the mental model that else was linked to the presence of a
break, but it always felt incomplete. I would write this:

for x in sequence:
    block
else:
    # only if no break occurs


and then I would feel guilty for lying, because that comment is not, in
fact, correct. Returning out of the function from inside the loop will also
avoid running the else part, as will an exception. If you think about it,
there are other ways that will prevent the else from running too. I leave
them as an exercise for the reader :-)


-- 
Steven



More information about the Python-list mailing list