13

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?


See also: Is it possible to make a `for` loop without an iterator variable? (How can I make code loop a set number of times?) for cases where the calculation is just a means to the end of looping a fixed number of times.

4 Answers 4

29

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.

Sign up to request clarification or add additional context in comments.

2 Comments

+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
Aside from a generator, one could take the approach that Python 3.x's built-in range does, and use a class that implements __getitem__ and __len__ by doing some math. This way, the same instance can be reused, as it's an iterable rather than just an iterator - see also Why can't I iterate twice over the same iterator? How can I "reset" the iterator or reuse the data?. In 2009, this would have been supported, but the underlying idiom would (I suppose) have been in some sense less familiar.
2

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.

1 Comment

WTF? Why not just use for i in xrange(begin, end):?
1

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)

Comments

-2

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

2 Comments

-1 Test answers before you publish. Result is TypeError: an integer is required. All args of xrange() must be integers.
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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.