Reputation: 71044
Suppose that I am looping over a iterable and would like to take some action if the iterator is empty. The two best ways that I can think of to do this are:
for i in iterable:
# do_something
if not iterable:
# do_something_else
and
empty = True
for i in iterable:
empty = False
# do_something
if empty:
# do_something_else
The first depends on the the iterable being a collection (so useless for when the iterable gets passed into the function/method where the loop is) and the second sets empty
on every pass through the loop which seems ugly.
Is there another way that I'm missing or is the second alternative the best? It would be really cool if there was some clause that I could add to the loop statement that would handle this for me much like else
makes not_found
flags go away.
I am not looking for clever hacks.
I am not looking for solutions that involve a lot of code
I am looking for a simple language feature. I am looking for a clear and pythonic way to iterate over an iterable and take some action if the iterable is empty that any experienced python programmer will be understand. If I could do it without setting a flag on every iteration, that would be fantastic. If there is no simple idiom that does this, then forget about it.
Upvotes: 6
Views: 531
Reputation: 24662
The general way forward if an iterator is to be partially checked before being consumed is to use itertools.tee
. This way we can have two copies of the iterator and check one for emptiness while still consuming the other copy from the start.
from itertools import tee
it1, it2 = tee(iterable)
try:
it1.next()
for i in it2:
do_some_action(i) #iterator is not empty
except StopIteration:
do_empty_action() #iterator is empty
The StopIteration
exception is bound to be a result of the call to it1.next()
, as any StopIteration
exceptions raised froom inside the loop will terminate that loop.
Edit: for those who don't like such exceptions, islice
can be used to set up a single step loop:
from itertools import tee, islice
it1, it2 = tee(iterable)
for _ in islice(it1, 1):
#loop entered if iterator is not empty
for i in it2:
do_some_action(i)
break #if loop entered don't execute the else section
else:
do_empty_action()
I personally prefer the first style. YMMV.
Upvotes: 1
Reputation: 107666
I think this the the cleanest way to do this:
# first try with exceptions
def nonempty( iter ):
""" returns `iter` if iter is not empty, else raises TypeError """
try:
first = next(iter)
except StopIteration:
raise TypeError("Emtpy Iterator")
yield first
for item in iter:
yield item
# a version without exceptions. Seems nicer:
def isempty( iter ):
""" returns `(True, ())` if `iter` if is empty else `(False, iter)`
Don't use the original iterator! """
try:
first = next(iter)
except StopIteration:
return True, ()
else:
def iterator():
yield first
for item in iter:
yield item
return False, iterator()
for x in ([],[1]):
# first version
try:
list(nonempty(iter(x))) # trying to consume a empty iterator raises
except TypeError:
print x, "is empty"
else:
print x, "is not empty"
# with isempty
empty, it = isempty(iter(x))
print x, "is", ("empty" if empty else "not empty")
Upvotes: 3
Reputation: 1942
Generators have a 'gi_frame' property which is None once the generator is exhausted, but only after StopIteration has been raised. If that's acceptable, here's something you could try:
import types
def do(x, f, f_empty):
if type(x) == types.GeneratorType:
# generators have a 'gi_frame' property,
# which is None once the generator is exhausted
if x.gi_frame:
# not empty
return f(x)
return f_empty(x)
if x:
return f(x)
return f_empty(x)
def nempty(lst):
print lst, 'not empty'
def empty(lst):
print 'Twas empty!'
# lists
do([2,3,4], nempty, empty)
do([], nempty, empty)
# generators
do((i for i in range(5)), nempty, empty)
gen = (i for i in range(1))
gen.next()
try:
gen.next()
except StopIteration:
pass
do(gen, nempty, empty)
Upvotes: 0
Reputation: 16318
if not map(do_something_callable,iterable) :
# do something else
Upvotes: 2
Reputation: 880239
This is a combination of Michael Mrozek's and FM's answers:
def with_divisible(n, a, b, f):
'''apply f to every integer x such that n divides x and a <= x < b'''
it = (i for i in xrange(a, b) if not i % n)
for i in it:
f(i)
try: i # test if `it` was empty
except NameError: print('do something else')
def g(i):
print i,
with_divisible( 3, 1, 10, g) # Prints 3 6 9.
with_divisible(33, 1, 10, g) # Prints "do something else"
Upvotes: 0
Reputation: 74745
Update 2
I liked Odomontois' answer. IMHO it is better suited to this problem than what I have written below.
Update
(After reading the OP's comment and edited question) You can do that too. See below:
def with_divisible(n, a, b, f):
it = (i for i in xrange(a, b) if not i % n)
for i in wrapper(it):
f(i)
>>> with_divisible(1, 1, 1, lambda x: x)
Traceback (most recent call last):
File "<pyshell#55>", line 1, in <module>
with_divisible(1, 1, 1, lambda x: x)
File "<pyshell#54>", line 3, in with_divisible
for i in wrapper(it):
File "<pyshell#46>", line 4, in wrapper
raise EmptyIterableException("Empty")
EmptyIterableException: Empty
>>> with_divisible(7, 1, 21, lambda x: x)
7
14
...Snipped...
raise EmptyIterableException("Empty")
EmptyIterableException: Empty
Original Answer
Interesting problem. I did some experiments and came up with the following:
class EmptyIterableException(Exception):
pass
def wrapper(iterable):
for each in iterable:
yield each
raise EmptyIterableException("Empty")
try:
for each in wrapper(iterable):
do_something(each)
except EmptyIterableException, e:
do_something_else()
Upvotes: 2
Reputation: 2223
What about reversing "if" and "for":
if iterable:
for i in iterable:
do_something(i)
else:
do_something_else()
OK, this does not work!
Here is an other solution: http://code.activestate.com/recipes/413614-testing-for-an-empty-iterator/
Upvotes: 0
Reputation: 175565
This is quite hackish, but you can delete i
and then check if it exists after the loop (if not, the loop never happened):
try:
del i
except NameException: pass
for i in iterable:
do_something(i)
try:
del i
except NameException:
do_something_else()
I think that's probably uglier than just using a flag though
Upvotes: 3