13

In Python I can implement a loop with step counter and a stop condition as a classical case of for loop :

for i in range(50):
    result = fun(i)
    print(i, result)
    if result == 0: 
        break

where fun(x) is some arbitrary function from integers to integers.

I always in doubts if that is the best way to code it (Pythonically, and in terms of readability and efficiency) or is it better to run it as a while loop:

i = 0
result = 1
while result != 0 and i < 50:
    result = fun(i)
    print(i, result)
    i += 1

which approach is better? In particular - I'm concerned about the usage of break statement which doesn't feel right.

Dimgold
  • 2,748
  • 5
  • 26
  • 49
  • 1
    In For loop if you modify the control variable it has no effect but While loop. – Smart Manoj Jun 11 '17 at 07:29
  • That's right, but I'm not aiming to do so – Dimgold Jun 11 '17 at 07:31
  • 6
    They are both fine. The for-loop places emphasis on the items in the iterable and the while-loop places the emphasis on the break condition. It is up to the author of the code to decide which aspect is most essential for communicating the meaning of the code. If neither prevail, use the for-loop because it is more commonly used in Python. – Steven Rumbalski Jun 11 '17 at 07:37
  • 2
    The `for` loop should be more efficient (by a small margin) and there's nothing wrong with breaking out of loops if an inner condition is met. Anyhow, this is a matter of personal preference so you won't get the 'right answer' - only opinions. – zwer Jul 07 '17 at 12:06
  • 1
    It should also be noted that `for loops` are faster than `while loops`. Python for loops are implemented in C. While loops are implemented with Python. https://stackoverflow.com/questions/869229/why-is-looping-over-range-in-python-faster-than-using-a-while-loop – reticentroot Jul 14 '17 at 00:21

8 Answers8

15

The for loop is slightly more performant than the while because range() is implemented in C, meanwhile the += operation is interpreted and requires more operations and object creation/ destruction. You can illustrate the performance difference using the timeit module, for example:

from timeit import timeit

def for_func():
    for i in range(10000):
        result = int(i)
        if result == -1: 
            break

def while_func():
    i = 0
    result = 1
    while result != -1 and i < 10000:
        result = int(i)
        i += 1


print(timeit(lambda: for_func(), number = 1000))
# 1.03937101364
print(timeit(lambda: while_func(), number = 1000))
# 1.21670079231 

The for loop is arguably more Pythonic in the vast majority of cases when you wish to iterate over an iterable object. Furthermore, to quote the Python wiki: "As the for loop in Python is so powerful, while is rarely used except in cases where a user's input is required". There is nothing un-Pythonic about using a break statement per se.

Readability is mostly subjective, I would say the for loop is more readable too, but it probably depends on your previous programming background and experience.

Chris_Rands
  • 38,994
  • 14
  • 83
  • 119
6

There is a minute difference in performance. As the while loop performs an extra boolean check at every iteration, it is slightly slower.

However, for most functions you would like to assess, there is an alternative to using a loop in the first place, which is way faster. See example attached below. And I am certain there are more optimal implementations..

import time

def fun(step):
    return step == 50000000

t1 = time.time()
for i in range(500000000):
    result = fun(i)
#    print(i, result)
    if result == 1: 
        break
print("time elapsed", time.time() - t1)

t2 = time.time()
i = 0
result = 0
while result != 1 and i < 50000000:
    result = fun(i)
#    print(i, result)
    i += 1

print("and here", time.time() - t2)

import numpy as np

t3 = time.time()

foo = np.arange(500000000)
i = np.where(foo == 50000000)
print("Alternative", time.time()-t3)

time elapsed 11.082087516784668

and here 14.429940938949585

Alternative 1.4022133350372314

If you want to/have to use a loop however, the for-loop is in general the more Pythonic way to go, as is explained in this nice answer to a related question

EDIT: Chris Rands's answer below explains it from the C-code point of view, and uses a more accurate (although on such macro-levels as I forced this example to be, that would not really matter) timing module. Do read his answer as well.

Uvar
  • 3,372
  • 12
  • 25
  • Sounds weird, I was sure that ``for`` is implemented with ``while``. – Dimgold Jul 07 '17 at 15:44
  • You are right that a for-loop internally uses a while loop. But "while True", is 1 identity check versus a more expensive Boolean (statement 1 AND statement 2) check. – Uvar Jul 10 '17 at 09:28
  • @Dimgold if you are thinking that the `for` loop you have written is internally doing `i += 1` like a C for loop, it's not. try something like `for i in range(10): print(i) i+=1`, does the output surprise you? – Ryan Haining Jul 12 '17 at 00:03
  • @RyanHaining, sure, that not surprising. But let's say it was ``xrange()`` - would it be better then? – Dimgold Jul 12 '17 at 16:18
  • @Dimgold it's the same output either way, for the same reason. I was assuming this was python3 based on your original code where `range` does what `xrange` did in python2 – Ryan Haining Jul 12 '17 at 16:35
4

best way would be not to loop explicitly at all. itertools for the rescue:

from itertools import takewhile
it = ((x, f(x)) for x in range(50)) # first you define the resulting view
it = takewhile(lambda y: y[1] != 0, it) # define when to stop iterating
list(map(print, it)) # if you want to print it

+ it would be a lot a lot faster than looping

simpleranchero
  • 3,534
  • 1
  • 12
  • 11
  • Don't you enumerate the whole options at the second row? It is quite simple in this toy example but i'm not sure that it is useful when on each iteration there are numerous actions ( maybe even sequential) to be done – Dimgold Jul 11 '17 at 20:03
  • nope. This 2nd and 3rd line are generators, they are run only on 4th line when list comes in. – simpleranchero Jul 13 '17 at 07:55
  • 1
    If they want a list, this is fine, but it is not pythonic to use a structure like this for procedural calls like `print()` because you needlessly create a list of `None` values, see https://stackoverflow.com/questions/5753597/is-it-pythonic-to-use-list-comprehensions-for-just-side-effects – Chris_Rands Jul 13 '17 at 09:35
3

Looking purely at the bytecode, While-loop got 30 lines and the for-loop got 26 lines.

In the end, don't really think it matters that much but I personally prefer the for-loop version as it's easier to read.

http://mergely.com/9P1cfnTr/

Code I used:

def fun(i):
    i = i + 1
    return i

def test1():
    i = 0
    result = 1
    while result != 0 and i < 50:
        result = fun(i)
        print(i, result)
        i += 1

def test2():
    for i in range(50):
        result = fun(i)
        print(i, result)
        if result == 0: 
            break
StefanE
  • 7,578
  • 10
  • 48
  • 75
  • 1
    The number of line _per se_ does not matter, it is the line that **jump back** to the loop that matter at the end. So you cannot make any assumption by only the number of lines. – Kruupös Jul 10 '17 at 08:57
1

I'm pretty sure you should not care about performance when you are comparing while and for loops, so the main question is about readability.

first of all, of course

for i in range(50):
    result = fun(i)
    print(i, result)
    if result == 0: 
        break

is much better than

i = 0
result = 1
while result != 0 and i < 50:
    result = fun(i)
    print(i, result)
    i += 1

because you don't need to keep some C-like variables in mind when you are reading simple loop

@quidkid's solution is pretty elegant, but it is a good point that

it = takewhile(lambda y: y[1] != 0, ((x, f(x)) for x in range(50)))

works only on quite simple examples and will lost readability as you will require to add some additional functionality

As i can see, the main problem in your code is that your function is not pure, so maybe you should do something like this:

def some_generator():
    for i in range(50):
        result = fun(i)
        yield result, i
        if result == 0:
            return 

for x in some_generator():
    print x
    do_something_else_with(x[1])
Alex Pertsev
  • 931
  • 4
  • 13
1

Taking the functional approach, create a local closure that performs the action and returns the termination predicate's value. Then iterate over the inputs until done.

def action(i):
    result = fun(i)
    print(i, result)
    return result == 0

list(itertools.takewhile(action, range(50)))  # non-pythonic wasted list

Another way of terminating a sequence of actions is:

terminated = reduce(lambda done, i: done or action(i), range(50), False)  # extra unnecessary iterations

Looks like we are back to the break statement for pythonicness

for i in range(50):
    if action(i):
        break
Mike Robins
  • 1,733
  • 10
  • 14
1

If efficiency is what you are looking for then use generator functions

In [1]: def get_result():                            
    ...:     for i in xrange(50):                            
    ...:         result = fun(i)
    ...:         yield result, i
    ...:         if result == 0:
    ...:             break
    ...:         

In [2]: generator_func = get_result()

In [3]: for result, i in generator_func:
    ...:     print result, i
    ...:   

The result of %timeit in ipython

The slowest run took 6.15 times longer than the fastest. This could mean that an intermediate result is being cached. 10000 loops, best of 3: 30.9 µs per loop

theBuzzyCoder
  • 2,652
  • 2
  • 31
  • 26
-1

You do NOT need the break statement at all... for loops will terminate automatically. All you need is:

for i in range(50):
    result = fun(i)
    print(i, result)

This, will always be more readable imho.

quidkid
  • 598
  • 5
  • 5