9

The else block in a for/else clause gets executed if the iteration finishes but is not interrupted by break, so I read.

Is there a language construct which would let me write something which executes only if the for loop did not begin iteration? If I was using a tuple or list, I would do something like this:

if seq:
    for x in seq:
         # something
else:
    # something else

But when I use a generator, I don't get the behavior I want:

>>> g = (x for x in range(2))
>>> for x in g:
...     print x
... else:
...     print "done"
... 
0
1
done    # I don't want "done" here
>>> g = (x for x in range(2) if x > 1)
>>> if g:
...     for x in g:
...         print x
... else:
...     print "done"
... 
>>>     # I was expecting "done" here

How can I do this without exhausting creating a tuple or a list from the generator, while also using a for loop? I could use next() in a while loop and try to catch StopIteration, but I'd like to see if there's a nice way to do it with for.

2rs2ts
  • 10,662
  • 10
  • 51
  • 95
  • 1
    I'd probably set a `ran` flag inside the loop and use `if not ran:`. – user2357112 Aug 07 '13 at 19:27
  • 1
    You can't. See http://stackoverflow.com/questions/661603/how-do-i-know-if-a-generator-is-empty-from-the-start – Ludo Aug 07 '13 at 19:34
  • @Ludo I know that I can't know if a generator is empty to begin with. I was just wondering if there's a nice language construct to handle this case. – 2rs2ts Aug 07 '13 at 19:36
  • 1
    Great question. Came here for the same thing. I've been writing Python for > 7 years and just wanted to know if there was some syntactic sugar hiding in the language that I'd never discovered. Alas, there is not. – John Carrell Jun 26 '23 at 20:17

8 Answers8

6

I can't think of a better way than updating a boolean inside the for loop.

any_results = False
for x in g:
    any_results = True
    print x
if not any_results:
    print 'Done'
Peter DeGlopper
  • 36,326
  • 7
  • 90
  • 83
  • 2
    Yeah, I'd do that normally; I just wanted to know if there was a control structure or some other language construct which did this succinctly. – 2rs2ts Aug 07 '13 at 19:43
  • This is just like having a counter.Can't we use for...else provided by python? – ns15 Mar 22 '17 at 05:46
  • @shadow0359 - I don't see how, `for...else` on an empty `g` will exit with `true`. – Peter DeGlopper Mar 22 '17 at 15:12
6
n = -1
for n, i in enumerate(it):
    do_stuff()
if n < 0:
    print 'Done'
glglgl
  • 89,107
  • 13
  • 149
  • 217
5

I found this solution much better.Check this link for more info(http://python-notes.curiousefficiency.org/en/latest/python_concepts/break_else.html).

You can use a custom sentinel: x = no_data = object()

x = no_data = object()
for x in data:
    .......
if x is no_data:
    print "Loop did not run"

object() returns a featureless object which is a base for all classes.

is checks if both the objects are same(x is no_data).If they remain same ,means the control never went to the for loop.

ns15
  • 5,604
  • 47
  • 51
  • I think this may have some risks when x = no_data both points to a generator, cause a generator will exhaust after the for-loop. – Menglong Li Feb 22 '18 at 03:06
  • 1
    At first, `x` and `no_data` both point to an `object` instance. `data` is the generator from which `x` takes values from. If the loop ran then `x` will have changed and `x is no_data` will be surely False. In my opinion, there's no risk into it. – Stam Kaly Aug 29 '18 at 19:06
  • @Menglong what is the risk you are refering to? – ns15 Aug 30 '18 at 07:43
  • I think this is the cleanest solution for `for` loops. IMHO calling the sentinel object `no_data_sentinel` would make the code more clear. – pabouk - Ukraine stay strong Nov 17 '21 at 19:31
2

You can use a generator function:

next accepts an optional second argument, that can be used to specify a default value in case the iterator has exhausted.

def func(lis):
    g = (x for x in lis if x > 1)
    flag = object()      # expected to be unique
    nex = next(g, flag)  # will return flag if genexp is empty
    if nex is not flag:
        yield nex
        for item in g:
            yield item
    else:
        yield "done"

for x in func(range(2)):
    print x
print "------"
for x in func(range(4)):
    print x

Output:

done
------
2
3
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
  • How does this compare in efficiency with memory and of course execution time to either the boolean approach or doing `tuple(g)`? – 2rs2ts Aug 07 '13 at 19:42
  • @2rs2ts As iterators have some extra overhead so they are going to be slow compared to the boolean approach, but an advantage is that you can access limited number of items(i.e slicing) using `itertools.islice` without iterating over the all items(if needed in any case), you can't so that with a simple for-loop. Read [this](http://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results) for comparison between tuple/list and genexp. – Ashwini Chaudhary Aug 07 '13 at 19:47
  • That is *very* cool. I'll be using list comprehensions more often for smaller data sets now. Anyway, for my purposes since I do not need any slicing or other forms of iteration, I'll be doing the boolean approach. But between your answer and your comment this was incredibly useful. Thank you! – 2rs2ts Aug 07 '13 at 20:57
0

I think a nice way to know if the loop actually executed is by using a loop variable

lv= 1
for x in g:
    lv = lv+1
    print x
if (lv == 1):
    print 'Done'

My syntax might be wrong, cos I'm not a python guy..

Popsyjunior
  • 185
  • 10
0

You could write a wrapper that counts the number of iterations. It has the advantage that it works with more exotic enumerations. In python3, it would be something like:

import sys
from glob import iglob

class GenCount(object):

    def __init__(self, gen):
        self._iter = iter(gen)
        self.count = 0

    def __next__(self):
        val = self._iter.__next__()
        self.count += 1
        return val

    def __iter__(self):
       return self

c = GenCount(iglob(sys.argv[1]))
for fn in c:
    print(fn)
print(c.count)


c = GenCount(iglob(sys.argv[1]))
print([fn for fn in c])
print(c.count)
tdelaney
  • 73,364
  • 6
  • 83
  • 116
0

You can check whether x was defined in the loop.

for x in (y for y in range(2) if y > 1):
    print(x)

try:
    print(f'do something with {x}')
except NameError:
    print('the loop did not run')

However, make sure that x is not defined before the loop.

MiaPlan.de
  • 102
  • 8
-1

In the example here, do you need an extra construct?

caught = None
for item in x:
    caught = item
    print caught
if caught != None: print "done"

*Edited for OP commen*t

theodox
  • 12,028
  • 3
  • 23
  • 36