11

Is there a better way to write the following:

   row_counter = 0
   for item in iterable_sequence:
      # do stuff with the item

      counter += 1

   if not row_counter:
      # handle the empty-sequence-case

Please keep in mind that I can't use len(iterable_sequence) because 1) not all sequences have known lengths; 2) in some cases calling len() may trigger loading of the sequence's items into memory (as the case would be with sql query results).

The reason I ask is that I'm simply curious if there is a way to make above more concise and idiomatic. What I'm looking for is along the lines of:

for item in sequence:
   #process item
*else*:
   #handle the empty sequence case

(assuming "else" here worked only on empty sequences, which I know it doesn't)

Dmitry B.
  • 9,107
  • 3
  • 43
  • 64
  • a better way to write it - for what purpose? – canavanin Dec 29 '10 at 19:45
  • to make it more concise and idiomatic looking. (i'm going to add this to my question) – Dmitry B. Dec 29 '10 at 19:47
  • An ORM could answer that for you; something like DB-API's `.fetchone()` or `.first()` methods return `None` if the result set is empty (no rows). – jfs Dec 29 '10 at 19:55
  • See http://stackoverflow.com/questions/1952464/in-python-how-do-i-determine-if-a-variable-is-iterable – mtrw Dec 29 '10 at 19:58
  • 1
    Personally, I find the original code more readable and idiomatic than the answers given. (A boolean flag variable `is_empty` would also be appropriate.) @mtrw: iter([]) returns an iterator, but is an empty sequence. – tcarobruce Dec 29 '10 at 20:11

5 Answers5

8
for item in iterable:
    break
else:
    # handle the empty-sequence-case here

Or

item = next(iterator, sentinel)
if item is sentinel:
   # handle the empty-sequence-case here   

In each case one item is consumed if it is present.


An example of empty_adapter()'s implementation mentioned in the comments:

def empty_adaptor(iterable, sentinel=object()):
    it = iter(iterable)
    item = next(it, sentinel)
    if item is sentinel:
       return None # empty
    else:
       def gen():
           yield item
           for i in it:
               yield i
       return gen()

You could use it as follows:

it = empty_adaptor(some_iter)
if it is not None: 
   for i in it:
       # handle items
else:
   # handle empty case

Introducing special case for an empty sequence for a general case seems wrong. There should be a better solution for a domain specific problem.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • 2
    I need to process all items in the list, not just the first one. To process the rest of the items, I'll need to set up another for loop, which will make the entire solution even less concise than the original. – Dmitry B. Dec 29 '10 at 19:54
  • Looks like consuming an item is not an option for the O.P. since h e has to "do stuff" with each item. – jsbueno Dec 29 '10 at 19:56
  • @Dmitry Beransky: if you need to consume all items then you could write an adapter that accepts an iterable as an input and returns `None` if it is empty or yields the same sequence of items as its input. For the implementation you could use the most readable variant among provided in this question so far. – jfs Dec 29 '10 at 20:12
3

It may be a job for itertools.tee You "trigger" the sequence on the verification, but you are left with an untouched copy of the sequence afterwards:

from itertools import tee
check, sequence = tee(sequence, 2)

try:
   check.next():
except StopIteration:
   #empty sequence
for item in sequence:
     #do stuff

(it's worth nting that tee does the "right" thing here: it will load just the first element of the sequence in the moment check.next() is executed - and this first elment will remain available in the sequence. The remaining items will only be retrieved as part of the for loop Or just keeping it simple: If you can't use len, you can't check if the sequence has a bool value of True, for the same reasons.

Therefore, your way seens simple enough - another way would be to delete the name "item" before the "for" statement and check if it exists after the loop:

del item
for item in sequence:
    # do stuff
try:
    item
except NameError:
     # sequence is empty.

But your code should be used as its more clear than this.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • `del item` will `raise NameError` if the name `item` wasn't already defined. – Karl Knechtel Dec 29 '10 at 20:08
  • Using ``tee`` is probably not the way to go - it will buffer all elements, expecting ``check`` to be iterated as well. For a finite-length iterable, ``list`` may be more efficient, for an infinite-length iterable it will silently leak memory. Checking for ``NameError`` is brilliant, though. – MisterMiyagi Sep 20 '17 at 15:53
2

The second example from J.F. Sebastian seems to be the ticket with a while loop.

NoItem = object()
myiter = (x for x in range(10))
item = next(myiter, NoItem)
if item is NoItem:
    ...
else:
    while item is not NoItem:
        print item
        item = next(myiter, NoItem)

Not the most concise but objectively the clearest... Mud, no?

jlujan
  • 1,188
  • 7
  • 10
2

This shouldn't trigger len():

def handle_items(items):
    index = -1
    for index, item in enumerate(items):
        print 'processing item #%d: %r' % (index, item)
    # at this point, index will be the offset of the last item,
    # i.e. length of items minus one
    if index == -1:
        print 'there were no items to process'
    print 'done'
    print

# test with an empty generator and two generators of different length:
handle_items(x for x in ())
handle_items(x for x in (1,))
handle_items(x for x in (1, 2, 3))
akaihola
  • 26,309
  • 7
  • 59
  • 69
0
if not iterable_sequence.__length_hint__():
    empty()
else:
    for item in iterable_sequence:
        dostuff()
Cees Timmerman
  • 17,623
  • 11
  • 91
  • 124
Falmarri
  • 47,727
  • 41
  • 151
  • 191
  • wouldn't this trigger a call to len()? – Dmitry B. Dec 29 '10 at 19:53
  • It would trigger a call to len() for sequences that are already evaluated, which is no problem - it wont call `len` on other iterables - because they don't generally have a `len` anyway. However, those iterables will always test as True unless they've specially overloaded that operation (as `xrange` does). – Karl Knechtel Dec 29 '10 at 20:07