satoru
satoru

Reputation: 33225

Check if a generator really generate something in one line

Many functions of a library I use return generators instead of lists when the result is some collection.

Sometimes I just want to check if the result is empty of not, and of course I can't write something like:

   result = return_generator()
   if result:
      print 'Yes, the generator did generate something!'

Now I come up with a one-liner that solve this problem without consuming the generator:

   result = return_generator()
   if zip("_", result):
      print 'Yes, the generator did generate something!'

I wonder if there are cleaner ways to solve this problem in one line?

Upvotes: 2

Views: 448

Answers (4)

Rusty Rob
Rusty Rob

Reputation: 17173

here is one way using itertools tee function which duplicates the generator:

from itertools import tee
a, b = tee(xrange(0))
try:
    next(a)
    print list(b)
except StopIteration:
    print "f1 was empty"


a, b = tee(xrange(3))
try:
    next(a)
    print list(b)
except StopIteration:
    print "f2 was empty"

>>>
[0, 1, 2, 3]
f2 was empty

Upvotes: 4

Rusty Rob
Rusty Rob

Reputation: 17173

Here are the two most simple solutions for your situation that I can think of:

def f1():
    return (i for i in range(10))

def f2():
    return (i for i in range(0))


def has_next(g):
    try:
        from itertools import chain
        return chain([g.next()],g)
    except StopIteration:
        return False

g = has_next(f1())
if g:
    print list(g)

g = has_next(f2())
if g:
    print list(g)


def has_next(g):
    for i in g:
        return chain([i],g)
    return False

g = has_next(f1())
if g:
    print list(g)

g = has_next(f2())
if g:
    print list(g)

Upvotes: 1

glglgl
glglgl

Reputation: 91029

This zip stuff eats the first item yielded, so it is not a good idea as well.

You can only detect if a generator has an item yielding by getting and keeping it until needed. The following class will help you to do so.

If needed, it gets an item from the iterator and keeps it.

If asked for emptyness (if myiterwatch: ...), it tries to get and returns if it could get one.

If asked for the next item, it will return the retrieved one or a new one.

class IterWatch(object):
    def __init__(self, it):
        self.iter = iter(it)
        self._pending = []
    @property
    def pending(self):
        try:
            if not self._pending:
                # will raise StopIteration if exhausted
                self._pending.append(next(self.iter))
        except StopIteration:
            pass # swallow this
        return self._pending
    def next(self):
        try:
            return self.pending.pop(0)
        except IndexError:
            raise StopIteration
    __next__ = next # for Py3
    def __iter__(self): return self
    def __nonzero__(self):
        # returns True if we have data.
        return not not self.pending
        # or maybe bool(self.pending)
    __bool__ = __nonzero__ # for Py3

This solves the problem in a very generic way. If you have an iterator which you just want to test, you can use

guard = object()
result = return_generator()
if next(result, guard) is not guard:
    print 'Yes, the generator did generate something!'

next(a, b) returns b if iterator a is exhausted. So if it returns the guard in our case, it didn't generate something, otherwise it did.

But your zip() approach is perfectly valid as well...

Upvotes: 3

synthesizerpatel
synthesizerpatel

Reputation: 28036

Instead of returning the generator, just use it? Here's an example of a generator that may or may not return results - if there are results action is taken, if there are no results no action is taken.

#!/usr/bin/python

import time

def do_work():
    if int(time.time()) % 2:
        for a in xrange(0,100):
            yield a

for thing in do_work():
    print thing

Upvotes: 2

Related Questions