Creative looping

Wednesday 30 July 2014This is over ten years old. Be careful.

One of the interesting things about helping beginning programmers is to see the way they think. After programming for so long, and using Python for so long, it’s hard to remember how confusing it can all be. Beginners can reacquaint us with the difficulties.

Python has a handy way to iterate over all the elements of a sequence, such as a list:

for x in seq:
    doit(x)

But if you’ve only learned a few basic things, or are coming from a language like C or Javascript, you might do it like this:

i = 0
while i < len(seq):
    x = seq[i]
    doit(x)
    i += 1

(BTW, I did a talk at the PyCon before last all about iteration in Python, including these sorts of comparisons of techniques: Loop Like a Native.)

Once you learn about the range() builtin function, you know you can loop over the indexes of the sequence like this:

for i in range(len(seq)):
    x = seq[i]
    doit(x)

These two styles of loop are commonly seen. But when I saw this on Stackoverflow, I did a double-take:

i = 0
while i in range(len(seq)):
    x = seq[i]
    doit(x)
    i += 1

This is truly creative! It’s an amalgam of the two beginner loops we’ve already seen, and at first glance, looks like a syntax error.

In fact, this works in both Python 2 and Python 3. In Python 2, range() produces a list, and lists support the “in” operator for checking element membership. In Python 3, range() produces a range object which also supports “in”.

So each time around the loop, a new range is constructed, and it’s examined for the value of i. It works, although it’s baroque and performs poorly in Python 2, being O(n2) instead of O(n).

People are creative! Just when I thought there’s no other ways to loop over a list, a new technique arrives!

Comments

[gravatar]
"So each time around the loop, a new range is constructed, and it's examined for the value of i."

Are you sure about that? What python version? In my experimentations, the range is constructed just once.
[gravatar]
If you need the index inside the for loop, another way is using enumerate():
for i, x in enumarate(seq):
    doit(seq[i])
Just another way to do it (-:
[gravatar]
@Matt the expression in the while statement is evaluated each time around the loop. That expression calls range(), which will make a new range object. How are you determining that it's the same object?
[gravatar]
I thought what Matt said was the case as well, since this appears to be the case in for loops. I ran an experiment to see for myself:
>>> def prange(x):
...     print("Ran prange")
...     return range(x)
... 
>>> def plen(obj):
...     print("Ran plen")
...     return len(obj)
... 
>>> i = 0
>>> seq = [1,2,3,4,5]
>>> while i in prange(plen(seq)):
...     x = seq[i]
...     print("Loop" + str(i))
...     i += 1
...
Ran plen
Ran prange
Loop 0
Ran plen
Ran prange
Loop 1
Ran plen
Ran prange
Loop 2
Ran plen
Ran prange
Loop 3
Ran plen
Ran prange
Loop 4
Ran plen
Ran prange
>>> for i in prange(plen(seq)):
...     print("Loop " + str(i))
... 
Ran plen
Ran prange
Loop 0
Loop 1
Loop 2
Loop 3
Loop 4
So it does indeed appear that in this while loop construction, the functions are evaluated at the beginning of every loop. Note, this is using Python 2.7.5, maybe it's different in 3?
[gravatar]
@AK: for loops and while loops are different. In a for loop, the expression is evaluated once to get an iterator. Then the body of the loop is executed once for each value pulled from the iterator. In a while loop, the expression is evaluated each time around the loop. It's the same in all versions of Python.
[gravatar]
One of the things that makes this code so confusing is that "while i in range(len(seq))" looks almost identical to "for i in range(len(seq))", the only difference is for and while.

But the for-statement syntax is "for VAR in EXPR:", and the while-statement syntax is "while EXPR:". The "in" keyword is part of the syntax of the for-statement, but part of the expression in the while-statement.
[gravatar]
Ah okay, interesting, thanks for the clarification!
[gravatar]
... argh ... silly typo on my part. You are right! Thanks for responding.

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.