Python execute code only if for loop did not begin iteration (with generator)?
Asked Answered
Z

9

11

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.

Zante answered 7/8, 2013 at 19:23 Comment(4)
I'd probably set a ran flag inside the loop and use if not ran:.Gileadite
You can't. See #662103Medor
@Medor 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.Zante
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.Tetany
B
8

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'
Benignant answered 7/8, 2013 at 19:35 Comment(3)
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.Zante
This is just like having a counter.Can't we use for...else provided by python?Flagging
@shadow0359 - I don't see how, for...else on an empty g will exit with true.Benignant
A
6
n = -1
for n, i in enumerate(it):
    do_stuff()
if n < 0:
    print 'Done'
Allure answered 7/8, 2013 at 19:40 Comment(1)
i like the use of enumerate.Outride
F
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.

Flagging answered 22/3, 2017 at 6:8 Comment(4)
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.Mesitylene
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.Jerrome
@Menglong what is the risk you are refering to?Flagging
I think this is the cleanest solution for for loops. IMHO calling the sentinel object no_data_sentinel would make the code more clear.Veinule
A
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
Acrogen answered 7/8, 2013 at 19:38 Comment(3)
How does this compare in efficiency with memory and of course execution time to either the boolean approach or doing tuple(g)?Zante
@Zante 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 for comparison between tuple/list and genexp.Acrogen
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!Zante
Z
1

When the else branch is an expression (or a sequence of expressions) with side-effect, you can use the short-circuit property of the boolean operators. For instance, this Python 3 function:

def print_items_or_nothing(items):
    if items == []:
        print("nothing")
    else:
        for item in items:
            print(item)

print_items_or_nothing([])          # prints "nothing"
print_items_or_nothing([1, 2, 3])   # prints "1", "2", "3"

... can be rewritten as:

def print_items_or_nothing(items):
    for item in items or print("nothing") or []:
        print(item)

Explanation

This technique leverages the fact that or evaluates and returns its right operand if and only if the left one is falsy.

Consider the expression items or print("nothing") or []:

  • If items is non-empty, it is truthy, so the rest of the expression is not evaluated, and the result is items.
  • If items is empty, it is falsy, so print("nothing") is evaluated... to None, which is falsy, so [] is evaluated and constitutes the result, which is iterable (as required).

Advantages

  • no modification of the loop body;
  • no auxiliary variable;
  • lightweight.

Drawbacks

  • limited to the cases where the else branch contains no instruction (although you certainly can use a walrus operator for added unreadability);
  • clever ;)
Zink answered 26/2 at 21:58 Comment(0)
A
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..

Antidote answered 7/8, 2013 at 19:38 Comment(0)
D
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)
Diameter answered 7/8, 2013 at 20:10 Comment(0)
U
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.

Ungual answered 1/9, 2022 at 11:3 Comment(0)
A
-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

Aksel answered 7/8, 2013 at 19:40 Comment(1)
Nah, I want it to only print "done" if it didn't have anything to iterate over.Zante

© 2022 - 2024 — McMap. All rights reserved.