views:

636

answers:

4

Python's iterators are great and all, but sometimes I really do want a C-style for loop - not a foreach loop. For example, I have a start date and an end date and I want to do something for every day in that range. I can do this with a while loop, of course:

    current = start
    while current <= finish:
        do_stuff(current)
        current += timedelta(1)

This works, but it's 3 lines instead of 1 (in C or C-based languages) and I often find myself forgetting to write the incrementing line, especially if the loop body is quite complex. Is there a more elegant and less error-prone way of doing this in Python?

A: 

For the sake of iterating only, you should actually use xrange over range, since xrange will simply return an iterator, whereas range will create an actual list object containing the whole integer range from first to last-1 (which is obviously less efficient when all you want is a simple for-loop):

for i in xrange(current,finish+1, timedelta(1)):
    do_stuff(i)

Additionally, there is enumerate, which returns an enumerate object that will yield an incrementing count and the value of a collection, i.e.:

l = ["a", "b", "c"]
for ii, value in enumerate(l):
    print ii, value

Result:

0 a
1 b
2 c
scion
-1 Test answers before you publish. Result is `TypeError: an integer is required`. All args of `xrange()` must be integers.
John Machin
`xrange` should have been named `irange` as it returns an iterator while `range` should always return a list; the only constraint on `xrange` should be that `next=start; next=next+step; until next==end`, i.e. that `start` must be `__add__`able to `step` and the result must be `__cmp__`able to `end`
Dan D
+27  A: 

The elegant and Pythonic way to do it is to encapsulate the idea of a range of dates in its own generator, then use that generator in your code:

import datetime

def daterange(start, end, delta):
    """ Just like `range`, but for dates! """
    current = start
    while current < end:
        yield current
        current += delta

start = datetime.datetime.now()
end = start + datetime.timedelta(days=20)

for d in daterange(start, end, datetime.timedelta(days=1)):
    print d

prints:

2009-12-22 20:12:41.245000
2009-12-23 20:12:41.245000
2009-12-24 20:12:41.245000
2009-12-25 20:12:41.245000
2009-12-26 20:12:41.245000
2009-12-27 20:12:41.245000
2009-12-28 20:12:41.245000
2009-12-29 20:12:41.245000
2009-12-30 20:12:41.245000
2009-12-31 20:12:41.245000
2010-01-01 20:12:41.245000
2010-01-02 20:12:41.245000
2010-01-03 20:12:41.245000
2010-01-04 20:12:41.245000
2010-01-05 20:12:41.245000
2010-01-06 20:12:41.245000
2010-01-07 20:12:41.245000
2010-01-08 20:12:41.245000
2010-01-09 20:12:41.245000
2010-01-10 20:12:41.245000

This is similar to the answer about range, except that the built-in range won't work with datetimes, so we have to create our own, but at least we can do it just once in an encapsulated way.

Ned Batchelder
Very elegant indeed :)
Russell
+1 not only because it's the only answer that **actually works** but also bacause it's the right one. Seriously, don't vote up answers that just *look good*
THC4k
+1  A: 

Doing it in a compact way it's not easy in Python, as one of the basic concepts behind the language is not being able to make assignments on comparisons.

For something complex, like a date, I think that the answer of Ned is great, but for easier cases, I found very useful the itertools.count() function, which return consecutive numbers.

>>> import itertools
>>> begin = 10
>>> end = 15
>>> for i in itertools.count(begin):
...   print 'counting ', i
...   if i > end:
...     break
...
counting  10
counting  11
counting  12
counting  13
counting  14
counting  15
counting  16

I found it less error-prone, as it's easy, as you said, to forget the 'current += 1'. To me it seems more natural to make an infinite loop and then check for an end condition.

Khelben
WTF? Why not just use `for i in xrange(begin, end):`?
Chinmay Kanchi
A: 

This will work in a pinch:

def cfor(start, test_func, cycle_func):
    """A generator function that emulates the most common case of the C for
    loop construct, where a variable is assigned a value at the begining, then
    on each next cycle updated in some way, and exited when a condition
    depending on that variable evaluates to false. This function yields what
    the value would be at each iteration of the for loop.

    Inputs:
        start: the initial yielded value
        test_func: called on the previous yielded value; if false, the
                   the generator raises StopIteration and the loop exits.
        cycle_func: called on the previous yielded value, retuns the next
                    yielded value
    Yields:
        var: the value of the loop variable

    An example:

    for x in cfor(0.0, lambda x: x <= 3.0, lambda x: x + 1.0):
        print x    # Obviously, print(x) for Python 3

    prints out

    0.0
    1.0
    2.0
    3.0

    """
    var = start
    while test_func(var):
        yield var
        var = cycle_func(var)
ascent1729