[Python-ideas] Control Flow - Never Executed Loop Body

Andrew Barnert abarnert at yahoo.com
Tue Mar 22 19:56:31 EDT 2016


On Mar 22, 2016, at 16:21, Steven D'Aprano <steve at pearwood.info> wrote:
> 
>> On Tue, Mar 22, 2016 at 06:21:24PM +0100, Sven R. Kunze wrote:
>>> On 22.03.2016 16:09, Stephen J. Turnbull wrote:
>>> Chris Barker writes:
>>>> All that being said:
>>>> 
>>>> how about  "elempty"?, to go with elif?
>>> 
>>> -1 Not to my taste, to say the least.
>> 
>> Hmm, it seems there is no easy solution for this.
> 
> Possibly with the exception of the three or four previously existing 
> easy solutions :-)

The only question is whether any of them are obvious enough (even for novices). If not, we could argue whether any proposed change would be significantly _more_ obvious. And, if so, then the question is whether the change is worth the cost. But I think what we have is already obvious enough. (Especially since 90% of the time, when you need to do something special on empty, you explicitly have a sequence, not any iterable, so it's just "if not seq:".) So really, this proposal is really just asking for syntactic sugar that complicates the language in exchange for making some already-understandable code a little more concise, which doesn't seem worth it.

>> What do you think 
>> about an alternative that can handle more than empty and else?
>> 
>> for item in collection:
>>    # do for item
>> except NeverExecuted:
>>    # do if collection is empty
>> 
>> It basically merges "try" and "for" and make "for" emit EmptyCollection.
> 
> Does this mean that every single for-loop that doesn't catch 
> NeverExecuted (or EmptyCollection) will raise an exception?

Elsewhere he mentioned that EmptyCollection would be a subclass of StopIteration.

Presumably, every iterator type (or just the builtin ones, and hopefully "many" others?) would have special code to raise EmptyCollection if empty. Like this pseudocode for list_iterator:

    def __next__(self):
        if not self.lst:
            raise EmptyCollection
        elif self.i >= len(self.lst):
            raise StopIteration
        else:
            i = self.i
            self.i += 1
            return self.lst[self.i]

Or, alternatively, for itself would do this. The for loop bytecode would have to change to stash an "any values seen" flag somewhere such that if it sees StopIteration and hasn't seen any values, it converts that to an EmptyCollection. Or any of the other equivalents (e.g., the compiler could unroll the first PyIter_Next from loop from the rest of them to handle it specially). But this seems like it would add a lot of overhead and complexity to every loop whether desired or not.

> If not, then how will this work? Is this a special kind of 
> exception-like process that *only* operates inside for loops?
> 
> What will an explicit "raise NeverExecuted" do?

Presumably that's the same question as what an explicit raise StopIteration does. Just as there's nothing stopping you from writing a __next__ method that raises StopIteration but then yields more values of called again, there's nothing stopping you from raising NeverExecuted pathologically, but you shouldn't do so. M

>> So, independent of the initial "never executed loop body" use-case, one 
>> could also emulate the "else" clause by:
>> 
>> for item in collection:
>>    # do for item
>> except StopIteration:
>>    # do after the loop
> 
> That doesn't work, for two reasons:
> 
> (1) Not all for-loops use iterators. The venerable old "sequence 
> protocol" is still supported for sequences that don't support __iter__. 
> So there may not be any StopIteration raised at all.

I think there always is.

IIRC, PyObject_Iter (the C API function used by iter and by for loops) actually constructs a sequence iterator object if the object doesn't have tp_iter (__iter__ for Python types) but does have  tp_sequence (__getitem__ for Python types, but, e.g., dict has __getitem__ without having tp_sequence). And the for loop doesn't treat that sequence iterator any different from "real" iterators returned by __iter__; it just calls tp_next (__next__) until StopIteration. (And the "other half" of the old-style sequence protocol, that lets old-style sequences be reversed if they have a length, is similarly implemented by the C API underneath the reversed function.)

I'm on my phone right now, so I can't double-check any of the details, but I'm 80% sure they're all at least pretty close...

> (2) Even if StopIteration is raised, the for-loop catches it (in a 
> manner of speaking) and consumes it.
> 
> So to have this work, we would need to have the for-loop re-raise 
> StopIteration... but what happens if you don't include an except 
> StopIteration clause? Does every bare for-loop with no "except" now 
> print a traceback and halt processing? If not, why not?

I think this could be made to work: a for loop without an except clause handles StopIteration the same as today (by jumping to the else clause), but one that does have one or more except clauses just treats it like a normal exception.

Of course this would mean for/except/else is now legal but useless, which could be confusing ("why does my else clause no longer run when I add an 'except ValueError' clause?").

More generally, I think the fact that for/except StopIteration is almost but not quite identical to plain for would be confusing more often than helpful.

But I think it is a coherent proposal, even if it's not one I like. :)



More information about the Python-ideas mailing list