[Python-ideas] for/except/else

Nick Coghlan ncoghlan at gmail.com
Sat Mar 4 22:17:31 EST 2017


On 3 March 2017 at 18:47, Wolfgang Maier <
wolfgang.maier at biologie.uni-freiburg.de> wrote:

> On 03/03/2017 04:36 AM, Nick Coghlan wrote:
>
>> On 2 March 2017 at 21:06, Wolfgang Maier
>> <wolfgang.maier at biologie.uni-freiburg.de
>> <mailto:wolfgang.maier at biologie.uni-freiburg.de>> wrote:
>>
>>     - overall I looked at 114 code blocks that contain one or more breaks
>>>
>>
>>
>> Thanks for doing that research :)
>>
>>
>>     Of the remaining 19 non-trivial cases
>>>
>>>     - 9 are variations of your classical search idiom above, i.e.,
>>>     there's an else clause there and nothing more is needed
>>>
>>>     - 6 are variations of your "nested side-effects" form presented
>>>     above with debatable (see above) benefit from except break
>>>
>>>     - 2 do not use an else clause currently, but have multiple breaks
>>>     that do partly redundant things that could be combined in a single
>>>     except break clause
>>>
>>
>>
>> Those 8 cases could also be reviewed to see whether a flag variable
>> might be clearer than relying on nested side effects or code repetition.
>>
>>
> [...]
>
>
>> This is a case where a flag variable may be easier to read than loop
>> state manipulations:
>>
>>     may_have_common_prefix = True
>>     while may_have_common_prefix:
>>         prefix = None
>>         for item in items:
>>             if not item:
>>                 may_have_common_prefix = False
>>                 break
>>             if prefix is None:
>>                 prefix = item[0]
>>             elif item[0] != prefix:
>>                 may_have_common_prefix = False
>>                 break
>>         else:
>>             # all subitems start with a common "prefix".
>>             # move it out of the branch
>>             for item in items:
>>                 del item[0]
>>             subpatternappend(prefix)
>>
>> Although the whole thing could likely be cleaned up even more via
>> itertools.zip_longest:
>>
>>     for first_uncommon_idx, aligned_entries in
>> enumerate(itertools.zip_longest(*items)):
>>         if not all_true_and_same(aligned_entries):
>>             break
>>     else:
>>         # Everything was common, so clear all entries
>>         first_uncommon_idx = None
>>     for item in items:
>>         del item[:first_uncommon_idx]
>>
>> (Batching the deletes like that may even be slightly faster than
>> deleting common entries one at a time)
>>
>> Given the following helper function:
>>
>>     def all_true_and_same(entries):
>>         itr = iter(entries)
>>         try:
>>             first_entry = next(itr)
>>         except StopIteration:
>>             return False
>>         if not first_entry:
>>             return False
>>         for entry in itr:
>>             if not entry or entry != first_entry:
>>                 return False
>>         return True
>>
>>     - finally, 1 is a complicated break dance to achieve sth that
>>>     clearly would have been easier with except break; from typing.py:
>>>
>>
>>
> [...]
>
>
>> I think is another case that is asking for the inner loop to be factored
>> out to a named function, not for reasons of re-use, but for reasons of
>> making the code more readable and self-documenting :)
>>
>>
> It's true that using a flag or factoring out redundant code is always a
> possibility. Having the except clause would clearly not let people do
> anything they couldn't have done before.
> On the other hand, the same is true for the else clause - it's only
> advantage here is that it's existing already



I forget where it came up, but I seem to recall Guido saying that if he
were designing Python today, he wouldn't include the "else:" clause on
loops, since it inevitably confuses folks the first time they see it.
(Hence articles like mine that attempt to link it with try/except/else
rather than if/else).


> - because a single flag could always distinguish between a break having
> occurred or not:
>
> brk = False
> for item in iterable:
>     if some_condition:
>         brk = True
>         break
> if brk:
>     do_stuff_upon_breaking_out()
> else:
>     do_alternative_stuff()
>
> is a general pattern that would always work without except *and* else.
>
> However, the fact that else exists generates a regrettable asymmetry in
> that there is direct language support for detecting one outcome, but not
> the other.
>

It's worth noting that this asymmetry doesn't necessarily exist in the
corresponding C idiom that I assume was the inspiration for the Python
equivalent:

    int data_array_len = sizeof(data_array) / sizeof(data_array[0]);
    in idx = 0;
    for (idx = 0; idx < data_array_len; idx++) {
        if (condition(container[idx])) {
            break;
        }
    }
    if (idx < data_array_len) {
        // We found a relevant entry
    } else {
        // We didn't find anything
    }

In Python prior to 2.1 (when PEP 234 added the iterator protocol), a
similar approach could be used for Python's for loops:

    num_items = len(container):
    for idx in range(num_items):
        if condition(container[idx]):
            break
    if num_items and idx < num_items:
        # We found a relevant entry
    else:
        # We didn't find anything

However, while my own experience with Python is mainly with 2.2+ (and hence
largely after the era where "for i in range(len(container)):" was still
common), I've spent a lot of time working with C and the corresponding
iterator protocols in C++, and there it is pretty common to move the "entry
found" code before the break and then invert the conditional check that
appears after the loop:

    int data_array_len = sizeof(data_array) / sizeof(data_array[0]);
    int idx = 0;
    for (idx = 0; idx < data_array_len; idx++) {
        if (condition(container[idx])) {
            // We found a relevant entry
            break;
        }
    }
    if (idx >= data_array_len) {
        // We didn't find anything
    }

And it's *this* version of the C/C++ idiom that Python's "else:" clause
replicates.

One key aspect of this particular idiomatic structure is that it retains
the same overall shape regardless of whether the inner structure is:

    if condition(item):
        # Condition is true, so process the item
        process(item)
        break

or:

    if maybe_process_item(item):
        # Item was processed, so we're done here
        break

Whereas the "post-processing" model can't handle pre-composed helper
functions that implement both the conditional check and the item
processing, and then report back which branch they took.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20170305/eeb8fd7b/attachment.html>


More information about the Python-ideas mailing list