Earlier I coded up a search loop with a complicated “found” logic in two separate places and I was reminded of this issue. I resorted to found = True; break but I can see the benefit of a simpler construct.
Playing around with this a bit, I wonder how the bare if: would fare?
for x in data:
if condition(x):
break
if:
print("found")
else:
print("not found")
That won’t require a new soft keyword and is a syntax error if detached from the loop, so the risk of accidentally breaking the code by a stray line between for and if is mitigated somewhat. It’s also basically the same code I wrote today with found = False/found = True removed and if found: replaced by if:.
(Apologies if that’s been proposed somewhere in the 160 messages above)
I found myself using quite a lot this pattern when searching over multi-dimensional spaces. Those were extremely useful since you don’t need to use intermediate variables for exiting nested loops. But I don’t know how much code would benefit from that in Python.
It’s far tamer than a goto, and isn’t really that unusual. For example “labeled break and continue” are bog standard in Java. It’s a minor variation of the Python syntax I made up. Although in Java it’s also possible to break/continue labeled blocks (not limited to loops).
Curiously enough, goto is a reserved word in Java, but isn’t used for anything.
In any case, that’s a rat hole this topic doesn’t need to jump into .
That is an option, but the below is IMO more convenient.
for ... as outer:
for ... as inner:
break outer
The design is more general benefiting more than 1 thing
Less prone to bugs. E.g. inserting 1 more loop breaks the wrong loop
Simply more convenient and explicit
Of course it is highly subject to whether other benefits of this construct is deemed to be good solution to other problems, such as the one of this topic.
Personally, I think it is worth exploring the following a bit more.
It is one of the options in PEP above and also potentially addresses the issue that this thread is tackling.
for i in range(2) as outer:
for j in range(2) as inner:
if i == j == 1:
break outer
print('OUTER ' + ('EXHAUSTED' if outer.exhausted else 'BROKEN'))
print('INNER ' + ('EXHAUSTED' if inner.exhausted else 'BROKEN'))
# OUTER BROKEN
# INNER BROKEN
Also, a bit more on why is broken/if_break/ifbreak/etc… not correct naming/concept.
def foo():
for i in range(2) as loop:
return loop
loop = foo()
print(loop.broken) # False
Which gives impression that loop might have run its full course.
This is exactly why else with meaning “if loop has run its full course” makes sense.
And why adding if_break stop making sense for else.
If added, it implies the meaning of else to be “if loop has not been broken”.
I.e.:
for ...:
break
if_break:
# If loop has been broken
else:
# If loop has not been broken
However, “if loop has not been broken” is not correct.
As it reaches that place in case “loop has not been broken or interrupted in any other way”.
I don’t know what would be best. exhausted (synonym to meaning of existing else) or interrupted (synonym to existing not-else meaning), or something else.
But break/broken/etc is misaligned with the logic and might be prone to further issues, both technical and misguided learning.
Not sure about “not difficult” part given all the fuss about if/while-else, but I completely agree with the first part, at least in this domain.
That’s nothing special. That’s the same for any code that’s not reached because you returned before you got to it. Would you also say that an ordinary if condition: X doesn’t mean “do X if the condition is true”, that that’s incorrect and must be described as “do X if we get there at all and the condition is true”?
Even venerable old C is thinking about adding this in the C2y timeframe, see two recent competing proposals on this (1, 2)
While I appreciate that the proposal aims to stay as simple as possible, I think it’s a missed opportunity for API coherence to not also consider (and ideally solve) the “how do I break out of nested loops” question (exactly the question other languages like C are now asking themselves BTW).
The as searchloop part immediately seems pythonically self-explanatory to me, so this looks like a great direction, that – at minimum – shouldn’t be prevented/pessimized by a simpler approach (mainly w.r.t. how if_break would need to be adapted once there are named loops, though I guess if_break(searchloop): would then become the most natural next evolution step).
I think they’re orthogonal. An else: clause is currently unambiguously associated with a specific loop, and the proposed if_break: would also be. if_break: applies to the loop it’s a clause on. It doesn’t matter to that how break itself is spelled.
for ... as searchloop:
while True as inner:
break searchloop
if_break: # associated with "inner"
# xxxxxxxxxx will this execute?
if_break: # associated with "searchloop"
# this will certainly execute
would be a bit of a puzzle. My answer would be no: it’s not “inner” that was broken out of, it’s “searchloop”.
Actually the named loops provides an answer to this question by implicitly managing automatic flags, and have some additional advantages. Also, they do not require the introduction of a new keyword. Yet a question remain, which might be a question of convention.
I hope the following rewriting of the snippet above will be sufficientlty self-explanatory regarding these points :
for ... as searchloop:
if condition:
continue searchloop # remark: another way to handle nested loops
while True as inner:
break searchloop # -> set "flag" searchloop.broken to True
if inner.broken:
pass
# This will never execute because the break above stops the subsequent
# instructions within the current iteration of searchloop.
if inner.broken:
pass # will this execute ???
elif not inner.broken:
pass # remark : this makes an alternative to "for/else" for nested loops.
if searchloop.broken: # this will certainly execute
print("searchloop did break after"+str(searchloop.niter)+"iterations."
# remark : another "automatic flag" usage
for _ in items:
if condition:
break # This makes `break` a temporary boolean value `True` in the following if/elif/else clauses. Otherwise `break` is a temporary `False`.
if break and condition2:
...
elif (break and condition3) or condition4:
...
elif condition5:
...
else:
...
The original for/else is logically backward-compatible by implicitly adding an if break: pass before the elif/else clauses if an if-clause is absent.
for _ in items:
if condition:
break
if break:
# do nothing, and the if-clause is optional.
else:
# do something
This also makes the following example simpler.
for _ in items:
if condition:
break
elif condition2:
...
verses
for _ in items:
if condition:
break
else:
if condition2:
...
That has been suggested previously in this thread, multiple times and wasn’t well liked. You could go back and figure what exactly people didn’t like and argue against it.
One question remain, considering running this code :
for i in range(Ni) as loop_i:
for j in range(Nj) as loop_j:
for k in range(Nk) as loop_k:
break loop_i
loop_i.broken will then be True, what about loop_j.broken and loop_k.broken ?
I think they should stay False, but there should be also a value loop_j.ended (or loop_j.exhausted as @dg-pb wrote before) that would also remain False at this point.
Another alternative is to have a loop.running attribute, then at the breaking of the outer loop loop_i.running, loop_j.running, loop_k.running are set to False.