unintuitive for-loop behavior

Chris Angelico rosuav at gmail.com
Sat Oct 1 05:02:52 EDT 2016


On Sat, Oct 1, 2016 at 6:35 PM, Steve D'Aprano
<steve+python at pearwood.info> wrote:
> On Sat, 1 Oct 2016 02:39 am, Chris Angelico wrote:
>
>> On Sat, Oct 1, 2016 at 12:36 AM, Grant Edwards
>> <grant.b.edwards at gmail.com> wrote:
>
>>> In C99 a for loop has its own namespac:
> [...]
>
>> I believe that's the same semantics as C++ uses, and I agree, it's
>> very convenient. Among other things, it means that nested loops behave
>> more like they do in Python, with independent iterators:
>
> *scratches head*
>
> So let me see if I understand this...
>
> You're suggesting that C99, where for-loops have their own namespaces, is
> MORE like Python (where for-loops DON'T have their own namespace), than
> C89, which, like Python, DOESN'T give for-loops their own namespace?
>
> That's ... curious.

Well, when you put it like that, it does sound very curious!!

Here's how I read it:

# Python
def main():
    for i in range(5):
        print(i, end=": ")
        for i in range(3):
            print(i, end=" ")
        print("")

/* Classic C */
int main() {
    int i;
    for (i=0; i<5; ++i) {
        printf("%d:", i);
        for (i=0; i<3; ++i)
            printf(" %d", i);
        printf("\n");
}

//C++ or C99
int main() {
    for (int i=0; i<5; ++i) {
        printf("%d:", i);
        for (int i=0; i<3; ++i)
            printf(" %d", i);
        printf("\n");
}

The first and last have *independent iterators*. When you complete the
inner loop, the outer loop picks up where *it* left off, not where you
happen to have reassigned the iteration variable. The middle one is an
infinite loop.

> I'm not saying you're wrong: after spending three quarters of an hour trying
> to fix a six line (including one blank line) C program because I
> accidentally closed a comment with /* instead of */, I will believe
> anything about C[1]. If you tell me that void causes birth defects and
> printf is responsible for the police shootings of unarmed black men, I'll
> believe every word of it.

Did you turn all warnings on? Look for a "pedantic" mode. You'll get a
lot more information, assuming you're using a modern decent C
compiler. (GCC, for instance, will even warn about situations where it
looks like the braces and indentation don't match, thus bringing a
Python feature to C, more-or-less.)

>> int main(void)
>> {
>>     for (int i=0; i<5; ++i)
>>     {
>>         printf("%d:", i);
>>         for (int i=0; i<3; ++i)
>>             printf(" %d", i);
>>         printf("\n");
>>     }
>> }
>>
>> Now, granted, this is not something I would ever actually recommend
>> doing, and code review is absolutely justified in rejecting this...
>
> So let me see if I understand your argument... for-loop namespaces are good,
> because they let you write code that you personally wouldn't write and
> would, in fact, reject in a code review.
>
> O-kay.

Yes. In the first place, I wouldn't necessarily reject this code; it's
a minor issue only, thanks to the namespacing and nested scoping. It
does cost a little in clarity, and I would point it out to a student,
but in less trivial examples than this, it's really not that big a
deal.

*Without* namespacing to keep them apart, this is a serious problem.
Thanks to namespacing, it's only a minor problem (you've shadowed one
variable with another). It's comparable to this Python code:

def send_message(from, to, subj, body):
    str = "Mail: From " + from
    str += "\nRecipient: To " + to
    str += "\nSubject: " + subj
    str += "\n\n" + body
    send_raw_data(str)

Would you reject this in a code review? Maybe. I wouldn't yell at you
saying "shadowing is fine, you have no right to reject that". But it's
also not inherently a problem. The rules of name shadowing are
well-defined (in both languages) and designed to *permit*, not reject,
constructs like this.

>> but it's a lot better than pure function-scope variables, where you'd
>> get stuck in an infinite loop.
>
> That's the risk that you take when you have a C-style for loop and you
> modify the loop variable. There's nothing special about the inner loop in
> that regard:
>
>
> #include <stdio.h>     /* for printf */
> int main(void)
>   {
>     for (int i=0; i<5; ++i)
>       {
>         printf("%d:", i);
>         i = 0;
>       }
>   }
>
>
> Solution: don't do that.

Actually, I would call this version a feature. If you deliberately and
consciously reassign the iteration variable, it's usually because you
wanted to adjust the loop. Classic example: iterating through a series
of tokens, and sometimes needing to grab a second token. Python lets
you do this by explicitly calling iter() and next(); C lets you do
this by incrementing the loop counter (which works only with the
less-flexible looping style it has).

But when you didn't intend to reassign it, it's usually because you
had a local name that happened to collide. Sure, that can happen
anywhere, especially with abbreviated variable names (I once had the
amusing situation of trying to use "cursor" and "current" in the same
namespace, both abbreviated "cur"), but it's usually going to involve
some form of inner block construct. It mightn't be a 'for' loop - it
might be a 'while' loop, or it might not even be a loop at all, just
one branch of an 'if'. Unless, of course, you're customarily writing
thousand-line C programs with no control flow structures at all, in
which case I have a nice padded cell for you right over here...

> [1] Apparently useful error messages are one of those things C programmers
> eschew, like type safety, memory safety, and correctness.

Totally. Those error messages cost 0.0000001% in run time performance,
so they had to go.

In all seriousness, though... -Wall makes your C programming life a
lot more bearable.

ChrisA



More information about the Python-list mailing list